By Mitja Kolsek, 0patch Team
Two days ago, an interesting exploit by Zhiniang Peng and Chen Wu was published on GitHub. It's a simple Python script that you launch against any 32-bit Windows Server 2003 with WebDAV functionality enabled, and it executes calc.exe on that server. Needless to say, this exploit could easily be modified to download a malicious executable to the server and launch it, and we confirmed it can also be used against 64-bit servers. To fully compromise a server, the exploit would have to escalate its privileges from NETWORK SERVICE user to LOCAL SYSTEM, where tricks like this one or this one might come handy.
Now, what makes this exploit so interesting?
- According to Shodan, there are a little over 600,000 publicly accessible IIS 6.0 servers on the Internet, most of them likely running on Windows Server 2003. (This doesn't include an unknown number of servers not accessible from the Internet.)
- The extended support period for Windows Server 2003 ended 20 months ago, so there is no official security fix for this issue.
Fortunately, most 2003 servers don't have WebDAV functionality enabled, but it is likely enabled on many SharePoint Portal servers and sites that use it for remote Web authoring or similar. Iraklis Mathiopoulos ran a sample probe to determine how many of Internet-connected 2003 servers are WebDAV-enabled and found that approximately 10% of them are. So we seem to have about 60,000 servers out there exposed to this freshly published exploit.
Sure, some will say that everyone should have stopped using Windows Server 2003 long ago as it doesn't get security patches any more. But owners of these servers each have their own story, their own set of constraints to work within, and their own budgets that they would rather spend on something other than upgrading a server that works.
To help maintainers of Windows Server 2003 computers block almost inevitable attacks under these unfavorable circumstances, we decided to provide them a free solution: a micropatch for CVE-2017-7269, which they can apply on their machines not only without rebooting, but also without even restarting Internet Information Services. (For those new to 0patch, this is how we believe applying security patches should always look like.)
Let me describe the process of analyzing this vulnerability and creating a micropatch.
The first step in our analyses is always reproducing the exploit or proof-of-concept. In this case, it was trivial: exploit.py provided by Peng and Wu was simple and effective, and it reliably launched calc.exe on a local 32-bit Windows Server 2003.
A full-blown exploit is usually not ideal for analyzing the vulnerability; especially in a buffer overflow case, we would prefer the process crashing at its vulnerable location to launching the calculator. So I "dumbed down" the exploit by replacing all of its shell code with A's (0x41 bytes). I also used WinDbg's Global Flags to enable Page Heap on the vulnerable w3wp.exe process in hope to catch the buffer overflow immediately as it occurs.
Sure enough, after attaching to w3wp.exe and launching my simplified poc.py, an access violation was caught due to writing beyond an allocated memory block. The offending instruction was at location httpext!ScStoragePathFromUrl+0x360:
rep movs dword ptr es:[edi],dword ptr [esi]
Those familiar with Intel machine code will recognize rep movs as a memory copying procedure. So apparently the overflow occurs when one buffer is copied into another - which is not large enough. Setting a breakpoint at the beginning of the memory-copying code block revealed that a buffer pointed to by esi (source) contained a 433-wide-character string (i.e., 868 bytes including the trailing zero) "/aaaaaaaAAA...(many A's)...AAA>", which is the exact value of one of the tokens sent in the WebDAV request by poc.py. The destination buffer pointed to by edi, however, was approximately 50% too small to receive the string. This provided an initial hint that it could be a case of using wide (two-byte) characters but allocating a buffer based on the number of characters instead of the number of bytes they take.
Anyway, in order to find where the too-small destination buffer pointed to by edi was allocated, I used this nifty WinDbg extension !heap -p -a edi, which gave:
7c83d6d4 ntdll!RtlAllocateHeap+0x00000e9f
5b7e1a40 staxmem!MpHeapAlloc+0x000000f3
5b7e1308 staxmem!ExchMHeapAlloc+0x00000015
67125df9 httpext!CHeap::Alloc+0x00000017
67125ee1 httpext!ExAlloc+0x00000008
67125462 httpext!HrCheckIfHeader+0x0000013c
6712561e httpext!HrCheckStateHeaders+0x00000010
6711f659 httpext!CPropFindRequest::Execute+0x000000f0
6711f7c5 httpext!DAVPropFind+0x00000047
671296d2 httpext!CDAVExt::DwMain+0x0000012e
67117bc6 httpext!DwDavFSExtensionProc+0x0000003f
...
It seemed like the allocation of our destination buffer occurred in httpext!HrCheckIfHeader as a result of calling CStackBuffer::resize.
push [ebp-434h]
lea ecx, [ebp-430h]
call ?resize@?$CStackBuffer@G$0BAE@@@QAEPAGI@Z
test eax, eax
jz loc_155A2
So I set a breakpoint at the top of this code block to see what this resize was all about. It turned out that the local variable [ebp-434h] holds the length (in characters) of the input string, and the above code wants to resize an existing buffer to be able to hold this entire string.
My suspicion was confirmed: instead of specifying the number of bytes to allocate, the above resize call gets the number of characters - making the resulting buffer much too small. Looking around the above code, I found a similar code block where this calculation is done correctly, which further confirmed my analysis.
At this point, it was easy to decide how to patch: instead of pushing [ebp-434h] to stack as an argument to the resize function, we should push that same value multiplied by two and further increased by 2, to accommodate for the string-teminating zero. Since I didn't want to modify the local variable [ebp-434h] as it is also being used elsewhere, I decided to replace the push [ebp-434h] instruction with the following:
mov eax, dword [ebp-434h]
lea eax, [eax+eax+2]
push eax
So I wrote a .0pp file for this and built a 0patch using our 0patch Builder (which, by the way, anyone can download as part of 0patch Agent for Developers), then I relaunched poc.py to see if my micropatch fixed the bug.
To my surprise, w3wp.exe still had an access violation in the same place. After checking that I didn't snafoo the patch, I traced the flaw again in the same way as before, only to discover that another, almost identical, incorrect buffer allocation takes place elsewhere in the code. Specifically, in httpext!CParseLockTokenHeader::HrGetLockIdForPath.
This second flaw was logically identical to the first one, and could be patched with effectively the same patch code using an additional patchlet. It was time to give poc.py another try and see how IIS reacts to it with the two patchlets applied.
Triumphantly, IIS returned "HTTP/1.1 207 Multi-Status" instead of reporting internal server error, and it did the same when the original exploit.py was launched against it. This was it, the bug was fully patched.
In conclusion, let's see the source code of this micropatch.
;target: Windows Server Standard 2003 R2 32bit, httpext.dll version 6.0.3790.4518 32bit
MODULE_PATH "C:\WINDOWS\system32\inetsrv\httpext.dll"
PATCH_ID 269
PATCH_FORMAT_VER 2
VULN_ID 2287
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x00015451
JUMPOVERBYTES 6 ; eliminate the original "push dword ptr [ebp-434h]"
code_start
mov eax, dword [ebp-434h]
lea eax, [eax+eax+2]
push eax
code_end
patchlet_end
patchlet_start
PATCHLET_ID 2
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x0001574b
JUMPOVERBYTES 6 ; eliminate the original "push dword ptr [ebp-22Ch]"
code_start
mov eax, dword [ebp-22Ch]
lea eax, [eax+eax+2]
push eax
code_end
patchlet_end
MODULE_PATH "C:\WINDOWS\system32\inetsrv\httpext.dll"
PATCH_ID 269
PATCH_FORMAT_VER 2
VULN_ID 2287
PLATFORM win32
patchlet_start
PATCHLET_ID 1
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x00015451
JUMPOVERBYTES 6 ; eliminate the original "push dword ptr [ebp-434h]"
code_start
mov eax, dword [ebp-434h]
lea eax, [eax+eax+2]
push eax
code_end
patchlet_end
patchlet_start
PATCHLET_ID 2
PATCHLET_TYPE 2
PATCHLET_OFFSET 0x0001574b
JUMPOVERBYTES 6 ; eliminate the original "push dword ptr [ebp-22Ch]"
code_start
mov eax, dword [ebp-22Ch]
lea eax, [eax+eax+2]
push eax
code_end
patchlet_end
You can see that the patch comprises two patchlets, both containing the same three machine code instructions, just at different offsets into the vulnerable httpext.dll and with different offsets to the relevant stack-based local variable. Extremely lightweight, extremely unlikely to cause unwanted side effects, and extremely reviewable - just the way micropatching is supposed to be.
If you have 0patch Agent installed on your Windows Server 2003, patches ZP-269 and ZP-270, for 32-bit and 64-bit server, respectively, should already be present and applied. If not, you can download a free copy of 0patch Agent to protect your server from CVE-2017-7269. Note, however, that these patches are only applicable to a fully updated server with Service Pack 2 installed, as they were made specifically for 32-bit and 64-bit httpext.dll version 6.0.3790.4518. (Check your httpext.dll version in C:\Windows\System32\inetsrv.)
Finally, you should know that we're still in beta so keep an eye on your server and report any unusual events to us at support@0patch.com. Also let us know if you have some other version of httpext.dll and we'll try to port the patch for you.
@mkolsek
@0patch