I was testing for insecure file uploads, and it turns out if I append a period behind a filename, I could bypass the application’s filter for allow file types.
However, this could not be replicated by the Triage personal. When he downloaded the file, the period got transformed into an underscore instead. Turns out, we were using different browsers (I was using FireFox, he was using Chrome), so this difference in behavior must be attributed to the browser.
My first browser CVE!!!
… not.
Seeing how this may be a potential vulnerability on FireFox, I opened a ticket on https://bugzilla.mozilla.org and described the behavior to them.
What I felt was the vulnerability was, attackers could bypass file filters by appending a period at the end of the filename, and when the file was downloaded by the user, Windows would discard the period, and it would be transformed into an executable. Very bad!
But it turns out I was 20 years too late, and this potentially dangerous behavior of Windows stripping away periods was already discovered before I even used the computerhttps://bugzilla.mozilla.org/show_bug.cgi?id=267828
Putt Putt Saves the Zoo!
So ok no bug, but lets analyze how FireFox deals with this peculiar behavior anyway.
We don't want to save hidden files starting with a dot, so remove any leading periods. This is done first, so that the remainder will be treated as the filename, and not an extension. Also, Windows ignores terminating dots. So we have to as well, so that our security checks do "the right thing"
Great, this function addresses exactly the “bug” I opened the ticket for.
So what does .Trim() do, and how does it fix the 20 year old bug?
/**
* This method trims characters found in aSet from either end of the
* underlying string.
*
* @param aSet -- contains chars to be trimmed from both ends
* @param aTrimLeading
* @param aTrimTrailing
* @param aIgnoreQuotes -- if true, causes surrounding quotes to be ignored
* @return this
*/
void Trim(const std::string_view& aSet, bool aTrimLeading = true,
bool aTrimTrailing = true, bool aIgnoreQuotes = false);
aTrimLeading, aTrimTrailing defaults to true, while aIgnoreQuotes defaults to false, so when the function calls
// Determine the current extension for the filename.
int32_t dotidx = fileName.RFind(u".");
if (dotidx != -1) {
CopyUTF16toUTF8(Substring(fileName, dotidx + 1), extension);
}
Memory copying is always a nice place to look at from a security POV. Lets look at the implementation just for fun
Won’t go down too deep into the rabbit hole, but it seems to take into account the length of the source, so there’s likely some boundary checks happening. Feel free to dig deeper!
Once we have the file extension that is stored in the variable extension, it’s then passed to GetFromTypeAndExtension()
One interesting thing is that this function detects null characters, and says that a file extension should never have a null character. This could be useful for defeating attacks that append a null character at the end of the filename in order to bypass file extension checks, as with PHP Poison Null Byte (which the PHP engine fixed almost 10 years ago https://bugs.php.net/bug.php?id=39863)
Then it tries to get the file extension by calling GetTypeFromExtension
They have an ordered list of actions to take in order to get the mime-type of the file based on the extension. In the first function GetMIMETypeFromDefaultForExtension(), these are the default extensions and their mapped mime-types
One peculiar thing…. notice how there’s no exe? That’s because these are generic file types that are common across OS-es, while an exe file is specific to Windows.
The exe mapping is likely gotten from the second function in the list: GetMIMETypeFromOSForExtension . Following this function, it eventually calls SendGetMIMEInfoFromOS which does some IPC calling and deserialization. Honestly, I’m not sure what this does, but from the function name, it gets the mime-type from OS 🙂. In the case of .exe, the mime-type would be application/x-msdownload
Out the rabbit hole we come out of, and wielding in our hand is a shiny piece of information:
.exe == application/x-msdownload !!
But… this mime-type inference has absolutely nothing to do with this protection mechanism. That’s what rabbit holes are for right? (I’M SORRY). But what IS related is the trimming of the period which gets us from this
nsresult nsMIMEInfoWin::LaunchDefaultWithFile(nsIFile* aFile) {
// Launch the file, unless it is an executable.
bool executable = true;
aFile->IsExecutable(&executable);
if (executable) return NS_ERROR_FAILURE;
return aFile->Launch();
}
FireFox would launch downloaded files automatically unless they are an executable. This code assumes that the file is an executable, before checking if the file really is an executable by calling IsExecutable
This checks the extension of the file if its in sExecutableExts , which is the whole list of extensions in Windows that are considered executables, including our beloved .exe!
FireFox assumes that Windows only ignores periods at the end of the file, and therefore they only trim periods from the filename. But is that assumption correct?
Spoiler
Yes
We write a simple PowerShell script to try to create files ending with characters from the Ascii range 0x00-0x127
We also prepend the decimal number at the front so it’s easier for us to know which character is being tested.
Then we sort the files by Type in the folder to see which characters are ignored by Windows, and end up transmuting to a Text Document
Only two characters are ignored by Windows
Decimal 32 (hex 20, or a space character)
Decimal 46 (hex 2e, or a period)
Spaces are likely trimmed already from various text manipulation steps both on the web application and in FireFox, so the only real danger comes from a period . , and in this case we came to the conclusion that FireFox sufficiently defends against such attacks.