🔥

Firefox Security

Where it started

In a bug bounty.
notion image
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.
notion image
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.
notion image

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 computer https://bugzilla.mozilla.org/show_bug.cgi?id=267828
Putt Putt Saves the Zoo!
Putt Putt Saves the Zoo!
So ok no bug, but lets analyze how FireFox deals with this peculiar behavior anyway.

File name cleaning

notion image
In the comments
đź’ˇ
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?
Header file definition
/** * 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
fileName.Trim(".")
It’s actually calling
fileName.Trim(".", aTrimLeading = true, aTrimTrailing = true, aIgnoreQuotes = false)
Which is then implemented in this file:
đź’ˇ
Security Researchers! Please hack this and find your CVE
void nsTSubstring<T>::Trim(const std::string_view& aSet, bool aTrimLeading, bool aTrimTrailing, bool aIgnoreQuotes) { char_type* start = this->mData; char_type* end = this->mData + this->mLength; // skip over quotes if requested if (aIgnoreQuotes && this->mLength > 2 && this->mData[0] == this->mData[this->mLength - 1] && (this->mData[0] == '\'' || this->mData[0] == '"')) { ++start; --end; } if (aTrimLeading) { uint32_t cutStart = start - this->mData; uint32_t cutLength = 0; // walk forward from start to end for (; start != end; ++start, ++cutLength) { if ((*start & ~0x7F) || // non-ascii aSet.find(char(*start)) == std::string_view::npos) { break; } } if (cutLength) { this->Cut(cutStart, cutLength); // reset iterators start = this->mData + cutStart; end = this->mData + this->mLength - cutStart; } } if (aTrimTrailing) { uint32_t cutEnd = end - this->mData; uint32_t cutLength = 0; // walk backward from end to start --end; for (; end >= start; --end, ++cutLength) { if ((*end & ~0x7F) || // non-ascii aSet.find(char(*end)) == std::string_view::npos) { break; } } if (cutLength) { this->Cut(cutEnd - cutLength, cutLength); } } }
tldr; this function iteratively removes the specified character at the start and end of the filename
Also, if it encounters any non-ascii characters, it exits the loop
This means that if your filename has any periods at the front and end of it, they would be stripped away.
...test.exe...
becomes
test.exe

Papers Please

But trimming way the period was just one of the security measures taken by FireFox.
Following the same file that called fileName.Trim("."), we go further down to see that it tries to check what mime-type the file is
notion image
It first tries to determine the extension of the file
// 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
[[nodiscard]] inline bool CopyUTF16toUTF8(mozilla::Span<const char16_t> aSource, nsACString& aDest, const mozilla::fallible_t&) { return nscstring_fallible_append_utf16_to_utf8_impl( &aDest, aSource.Elements(), aSource.Length(), 0); }
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()
if (aFlags & VALIDATE_GUESS_FROM_EXTENSION) { nsAutoCString mimeType; if (!extension.IsEmpty()) { mimeService->GetFromTypeAndExtension(EmptyCString(), extension, getter_AddRefs(mimeInfo)); if (mimeInfo) { mimeInfo->GetMIMEType(mimeType); } }
Which is defined in this file. extension is passed as aFileExt to this function.
notion image
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
notion image
Which is defined here
notion image
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
notion image
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

Not EXE pls

Out the rabbit hole we come out of, and wielding in our hand is a shiny piece of information:
.exe == application/x-msdownload !!
notion image
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
bad.exe....
to this
bad.exe
And with that, the real protection is done here
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
nsLocalFile::IsExecutable(bool* aResult) { return LookupExtensionIn(sExecutableExts, ArrayLength(sExecutableExts), aResult); }
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!
notion image

Mi Hack

With that, lets try to hack it!
notion image
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.
(0..127) | % {$hexValue = $_; $filename = [string]$_ + "filename.txt" + [char]$hexValue; echo $_; new-item -itemtype file -name $filename}
notion image
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
notion image
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.