Nettitude discovered a Memory Leak turned Use after Free (UaF) bug in the Microsoft implementation of the L2TP VPN protocol. The vulnerability affects most server and desktop versions of Windows, dating back to Windows Server 2008 and Windows 7 respectively. This could result in a Denial of Service (DoS) condition or could potentially be exploited to achieve Remote Code Execution (RCE).

Please see the official Microsoft advisory for full details:

https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-30211

L2TP is a relatively uncommonly used protocol and sits behind an IPSEC authenticated tunnel by default, making the chances of seeing this bug in the wild extremely low. Despite the low likelihood of exploitation, analysis of this bug demonstrates interesting adverse effects of code which was designed to actually mitigate security risk.

L2TP and IPSEC

The default way to interact with an L2TP VPN on Windows Server is by first establishing an IPSEC tunnel to encrypt the traffic. For the purposes of providing a minimalistic proof of concept, I tested against Windows Server with the IPSEC tunnelling layer disabled, interacting directly with the L2TP driver. Please note however, it is still possible to trigger this bug over an IPSEC tunnelled connection.

For curious readers, disabling IPSEC can be achieved by setting the ProhibitIpSec DWORD registry key with a value of 1 under the following registry path:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RasMan\Parameters\

This will disable IPSEC tunnelling and allow L2TP to be interacted with directly over UDP. Not to discourage a full IPSEC + L2TP solution, but it does make testing the L2TP driver a great deal easier!

Vulnerability Details

The bug in question is a reference increment bug located in the rasl2tp.sys L2TP VPN protocol driver, and relates to how tunnel context structures are reused. Each established tunnel for a connection is allocated a context structure, and a unique tunnel is considered to be the pairing of both a unique tunnel ID and UDP + IP address.

When a client initiates an L2TP StartControlConnectionRequest for a tunnel ID that they have previously used on a source IP and port that the server has already seen, the rasl2tp driver will attempt to reuse a previously allocated structure as long as it is not in an unusable state or already freed. This functionality is handled by the SetupTunnel function when a StartControlConnectionRequest is made with no tunnel or session ID specified in the L2TP Header, and an assigned tunnel ID matching one that has already been used.

Pseudo code for the vulnerable section is as follows:

if ( !lpL2tpHeaderHasTunnelID )
{
// Tunnel Lookup function uses UDP address information as well as TunnelID to match a previous Tunnel Context structure
NewTunnel = TunnelCbFromIpAddressAndAssignedTunnelId(lpAdapterCtx, lpSockAddr, lpTunnelId);
if ( NewTunnel ) // if a match is found a pointer is returned else the return is NULL
{
if...
ReferenceTunnel(NewTunnel, 1); // This is the vulnerable reference count
KeReleaseSpinLock(&lpAdapterCtx->TunnelLock, lpAdapterCtx->TunnelCurIRQL);
return NewTunnel;
}
}

The issue is that the reference count does not have an appropriate dereference anywhere in the code. This means that it is possible for a malicious client to continually send StartControlConnectionRequests to increment the value indefinitely.

This creates two separate vulnerable conditions. Firstly, because the reference count can be far greater than it should be, it is possible for an attacker to abuse the issue to exhaust the memory resources of the server by spoofing numerous IP address and tunnel ID combinations and sending several StartControlConnectionRequests. This would keep the structures alive indefinitely until the server’s resources are exhausted, causing a denial of service. This process can be amplified across many nodes to accelerate the process of consuming server resources and is only limited by the bandwidth capacity of the server. In reality, this process may also be limited by other factors applied to network traffic before the L2TP protocol is handled.

The second vulnerable condition is due to logic in the DereferenceTunnel function responsible for removing tunnel references and initiating the underlying free operation. It is possible to turn this issue into a Use after Free (UaF) vulnerability, which could potentially then be used to achieve Remote Code Execution.

Some pseudo code for the logic that allows this to happen in the DereferenceTunnel function is as follows:

__int64 __fastcall DereferenceTunnel(TunnelCtx *TunnelCtx)
{
...
lpAdapterCtx = TunnelCtx->AdapterCtx;
lpTunnelCtx = TunnelCtx;
lpAdapterCtx->TunnelCurIRQL = KeAcquireSpinLockRaiseToDpc(&lpAdapterCtx->TunnelLock);
RefCount = --lpTunnelCtx->TuneelRefCount;
if ( !RefCount )
{
// This code path properly removes the Tunnel Context from a global linked list and handles state termination
...
}
KeReleaseSpinLock(&lpAdapterCtx->TunnelLock, lpAdapterCtx->TunnelCurIRQL);
if ( RefCount > 0 ) // This line is vulnerable to a signed integer overflow
return (unsigned int)RefCount;
...
lpTunnelCtx->TunnelTag = '0T2L';
ExFreePoolWithTag(&lpTunnelCtx[-1].TunnelVcListIRQL, 0);
...
return 0i64;
}

The second check of the reference count that would normally cause the function to return uses a signed integer for the reference count variable. This means using the reference increment bug we can cause the reference count value to overflow and become a negative number. This would cause the DereferenceTunnel function to free the target tunnel context structure without removing it from the global linked list.

The global linked list in question is used to store all the active tunnel context structures. When a UDP packet is handled, this global linked list is used to lookup the appropriate tunnel structure. If a freed structure was still present in the list, any UDP packet referencing the freed context structure’s ID would be able to gain access to the freed structure and could be used to further corrupt kernel memory.

Exploitation

Exploitation of this bug outside of just exhausting the memory resources of a target server could take a very long time and I suspect would not realistically be exploitable or viable. Since a reference count can only happen once per UDP packet and each UDP message has to be large enough to contain all prior network stack frames and the required L2TP (and IPSEC) messages, the total required throughput is huge and would almost definitely be detected as a denial of service (DoS) attack long before reaching the required reference count.

Conclusion

This leaves the question of why would a developer allow a reference count to be handled in this way, when it should only ever require a minimum value of 0?

The main reason for allowing a reference count to become a negative number is to account or check for code that over removes references, and would typically result in an unsigned overflow. This kind of programming is a way of mitigating the risk posed by the more likely situation that a reference count is over-decremented. However, a direct result is that the opposite situation then becomes much more exploitable and in this scenario results in a potential for remote code execution (RCE).

Despite this, the mitigation is still generally effective, and the precursors for exploitation of this issue are unlikely to be realistically exploitable. In a way, the intended mitigation works because even though the maximum possible impact is far greater, the likelihood of exploitation is far lower.

Timeline

  • Vulnerability Reported To Microsoft – 20 April 2022
  • Vulnerability Acknowledged – 20 April 2022
  • Patch In Development – 23 June 2022
  • Patch Released – 12 July 2022