Exploring ZIP Mark-of-the-Web Bypass Vulnerability (CVE-2022-41049)
Windows ZIP extraction bug (CVE-2022-41049) lets attackers craft ZIP files, which evade warnings on attempts to execute packaged files, even if ZIP file was downloaded from the Internet.
In October 2022, I've come across a tweet from 5th July, from @wdormann, who reported a discovery of a new method for bypassing MOTW, using a flaw in how Windows handles file extraction from ZIP files.
This sounded to me like a nice challenge to freshen up my rusty RE skills. The bug was also a 0-day, at the time. It has already been reported to Microsoft, without a fix deployed for more than 90 days.
What I always find the most interesting about vulnerability research write-ups is the process on how one found the bug, what tools were used and what approach was taken. I wanted this post to be like this.
Now that the vulnerability has been fixed, I can freely publish the details.
Background
What I found out, based on public information about the bug and demo videos, was that Windows, somehow, does not append MOTW to files extracted from ZIP files.
Mark-of-the-web is really another file attached as an Alternate Data Stream (ADS), named Zone.Identifier
, and it is only available on NTFS filesystems. The ADS file always contains the same content:
[ZoneTransfer]
ZoneId=3
For example, when you download a ZIP file file.zip
, from the Internet, the browser will automatically add file.zip:Zone.Identifier
ADS to it, with the above contents, to indicate that the file has been downloaded from the Internet and that Windows needs to warn the user of any risks involving this file's execution.
This is what happens when you try to execute an executable like a JScript file, through double-clicking, stored in a ZIP file, with MOTW attached.
Clearly the user would think twice before opening it when such popup shows up. This is not the case, though, for specially crafted ZIP files bypassing that feature.
Let's find the cause of the bug.
Identifying the culprit
What I knew already from my observation is that the bug was triggered when explorer.exe
process handles the extraction of ZIP files. I figured the process must be using some internal Windows library for handling ZIP files unpacking and I was not mistaken.
ProcessHacker revealed zipfldr.dll
module loaded within Explorer process and it looked like a good starting point. I booted up IDA with conveniently provided symbols from Microsoft, to look around.
ExtractFromZipToFile
function immediately caught my attention. I created a sample ZIP file with a packaged JScript file, for testing, which had a single instruction:
WScript.Echo("YOU GOT HACKED!!1");
I then added a MOTW ADS file with Notepad and filled it with MOTW contents, mentioned above:
notepad file.zip:Zone.Identifier
I loaded up x64dbg
debugger, attached it to explorer.exe
and set up a breakpoint on ExtractFromZipToFile
. When I double-clicked the JS file, the breakpoint triggered and I could confirm I'm on the right path.
CheckUnZippedFile
One of the function calls I noticed nearby, revealed an interesting pattern in IDA. Right after the file is extracted and specific conditions are meet, CheckUnZippedFile
function is called, followed by a call to _OpenExplorerTempFile
, which opens the extracted file.
Having a hunch that CheckUnZippedFile
is the function responsible for adding MOTW to extracted file, I nopped its call and found that I stopped getting the MOTW warning popup, when I tried executing a JScript file from within the ZIP.
It was clear to me that if I managed to manipulate the execution flow in such a way that the branch, executing this function is skipped, I will be able to achieve the desired effect of bypassing the creation of MOTW on extracted files. I looked into the function to investigate further.
I noticed that CheckUnZippedFile
tries to combine the TEMP folder path with the zipped file filename, extracted from the ZIP file, and when this function fails, the function quits, skipping the creation of MOTW file.
Considering that I controlled the filename of the extracted ZIP file, I could possibly manipulate its content to trigger PathCombineW
to fail and as a result achieve my goal.
PathCombineW
turned out to be a wrapper around PathCchCombineExW
function with output buffer size limit set to fixed value of 260
bytes. I thought that if I managed to create a really long filename or use some special characters, which would be ignored by the function handling the file extraction, but would trigger the length check in CheckUnZippedFile
to fail, it could work.
I opened 010 Editor, which I highly recommend for any kind of hex editing work, and opened my sample ZIP file with a built-in ZIP template.
I spent few hours testing with different filename lengths, with different special characters, just to see if the extraction function would behave in erratic way. Unfortunately I found out that there was another path length check, called prior to the one I've been investigating. It triggered much earlier and prevented me from exploiting this one specific check. I had to start over and consider this path a dead end.
I looked if there are any controllable branching conditions, that would result in not triggering the call to CheckUnZippedFile
at all, but none of them seemed to be dependent on any of the internal ZIP file parameters. I considered looking deeper into CheckUnZippedFile
function and found out that when PathCombineW
call succeeds, it creates a CAttachmentServices
COM objects, which has its three methods called:
CAttachmentServices::SetReferrer(unsigned short const * __ptr64)
CAttachmentServices::SetSource(unsigned short const * __ptr64)
CAttachmentServices::SaveWithUI(struct HWND__ * __ptr64)
I realized I am about to go deep down a rabbit hole and I may spend there much longer than a hobby project like that should require. I had to get a public exploit sample to speed things up.
Huge thanks you @bohops & @bufalloveflow for all the help in getting the sample!
Detonating the live sample
I managed to copy over all relevant ZIP file parameters from the obtained exploit sample into my test sample and I confirmed that MOTW was gone, when I extracted the sample JScript file.
I decided to dig deeper into SaveWithUI
COM method to find the exact place where creation of Zone.Identifier
ADS fails. Navigating through shdocvw.dll
, I ended up in urlmon.dll
with a failing call to WritePrivateProfileStringW
.
This is the Windows API function for handling the creation of INI configuration files. Considering that Zone.Identifier
ADS file is an INI file containing section ZoneTransfer
, it was definitely relevant. I dug deeper.
The search led me to the final call of NtCreateFile
, trying to create the Zone.Identifier
ADS file, which failed with ACCESS_DENIED
error, when using the exploit sample and succeeded when using the original, untampered test sample.
It looked like the majority of parameters were constant, as you can see on the screenshot above. The only place where I'd expect anything dynamic was in the structure of ObjectAttributes
parameter. After closer inspection and half an hour of closely comparing the contents of the parameter structures from two calls, I concluded that both failing and succeeding calls use exactly the same parameters.
This led me to realize that something had to be happening prior to the creation of the ADS file, which I did not account for. There was no better way to figure that out than to use Process Monitor, which honestly I should've used long before I even opened IDA 😛.
Backtracking
I set up my filters to only list file operations related to files extracted to TEMP directory, starting with Temp
prefix.
The test sample clearly succeeded in creating the Zone.Identifier
ADS file:
While the exploit sample failed:
Through comparison of these two listings, I could not clearly see any drastic differences. I exported the results as text files and compared them in a text editor. That's when I could finally spot it.
Prior to creating Zone.Identifier
ADS file, the call to SetBasicInformationFile
was made with FileAttributes
set to RN
.
I looked up what was that R
attribute, which apparently is not set for the file when extracting from the original test sample and then...
The R
file attribute stands for read-only
. The file stored in a ZIP file has the read-only attribute set, which is set also on the file extracted from the ZIP. Obviously when Windows tries to attach the Zone.Identifier
ADS, to it, it fails, because the file has a read-only attribute and any write operation on it will fail with ACCESS_DENIED
error.
It doesn't even seem to be a bug, since everything is working as expected 😛. The file attributes in a ZIP file are set in ExternalAttributes
parameter of the ZIPDIRENTRY
structure and its value corresponds to the ones, which carried over from MS-DOS times, as stated in ZIP file format documentation I found online.
4.4.15 external file attributes: (4 bytes)
The mapping of the external attributes is
host-system dependent (see 'version made by'). For
MS-DOS, the low order byte is the MS-DOS directory
attribute byte. If input came from standard input, this
field is set to zero.
4.4.2 version made by (2 bytes)
4.4.2.1 The upper byte indicates the compatibility of the file
attribute information. If the external file attributes
are compatible with MS-DOS and can be read by PKZIP for
DOS version 2.04g then this value will be zero. If these
attributes are not compatible, then this value will
identify the host system on which the attributes are
compatible. Software can use this information to determine
the line record format for text files etc.
4.4.2.2 The current mappings are:
0 - MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)
1 - Amiga 2 - OpenVMS
3 - UNIX 4 - VM/CMS
5 - Atari ST 6 - OS/2 H.P.F.S.
7 - Macintosh 8 - Z-System
9 - CP/M 10 - Windows NTFS
11 - MVS (OS/390 - Z/OS) 12 - VSE
13 - Acorn Risc 14 - VFAT
15 - alternate MVS 16 - BeOS
17 - Tandem 18 - OS/400
19 - OS X (Darwin) 20 thru 255 - unused
4.4.2.3 The lower byte indicates the ZIP specification version
(the version of this document) supported by the software
used to encode the file. The value/10 indicates the major
version number, and the value mod 10 is the minor version
number.
Changing the value of external attributes to anything with the lowest bit set e.g. 0x21
or 0x01
, would effectively make the file read-only with Windows being unable to create MOTW for it, after extraction.
Conclusion
I honestly expected the bug to be much more complicated and I definitely shot myself in the foot, getting too excited to start up IDA, instead of running Process Monitor first. I started with IDA first as I didn't have an exploit sample in the beginning and I was hoping to find the bug, through code analysis. Bottom line, I managed to learn something new about Windows internals and how extraction of ZIP files is handled.
As a bonus, Mitja Kolsek from 0patch asked me to confirm if their patch worked and I was happy to confirm that it did!
The patch was clean and reliable as seen in the screenshot from a debugger:
I've been also able to have a nice chat with Will Dormann, who initially discovered this bug, and his story on how he found it is hilarious:
I merely wanted to demonstrate how an exploit in a ZIP was safer (by way of prompting the user) than that *same* exploit in an ISO. Â So how did I make the ZIP? Â I:
1) Dragged the files out of the mounted ISO
2) Zipped them. That's it. Â The ZIP contents behaved the same as the ISO.
Every mounted ISO image is listing all files in read-only mode. Drag & dropping files from read-only partition, to a different one, preserves the read-only attribute set for created files. This is how Will managed to unknowingly trigger the bug.
Will also made me realize that 7zip extractor, even though having announced they began to add MOTW to every file extracted from MOTW marked archive, does not add MOTW by default and this feature has to be enabled manually.
I mentioned it as it may explain why MOTW is not always considered a valid security boundary. Vulnerabilities related to it may be given low priority and be even ignored by Microsoft for 90 days.
When 7zip announced support for MOTW in June, I honestly took for granted that it would be enabled by default, but apparently the developer doesn't know exactly what he is doing.
I haven't yet analyzed how the patch made by Microsoft works, but do let me know if you did and I will gladly update this post with additional information.
Hope you enjoyed the write-up!