<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Quarkslab's blog - LPE</title><link href="http://blog.quarkslab.com/" rel="alternate"></link><link href="http://blog.quarkslab.com/feeds/lpe.rss.xml" rel="self"></link><id>http://blog.quarkslab.com/</id><updated>2026-10-01T00:00:00+02:00</updated><entry><title>Cato VPN Client: Split-Tunnel and Privilege Escalation (CVE-2026-10739)</title><link href="http://blog.quarkslab.com/cato-vpn-client-split-tunnel-and-privilege-escalation-cve-2026-10739.html" rel="alternate"></link><published>2026-10-01T00:00:00+02:00</published><updated>2026-10-01T00:00:00+02:00</updated><author><name>Lucas Laise</name></author><id>tag:blog.quarkslab.com,2026-10-01:/cato-vpn-client-split-tunnel-and-privilege-escalation-cve-2026-10739.html</id><summary type="html">&lt;p&gt;Local privilege escalation in the Cato VPN Client through the split-tunnel upload flow, from named pipe to SYSTEM delete and Windows Installer rollback.&lt;/p&gt;</summary><content type="html">&lt;h1 id="introduction"&gt;Introduction&lt;/h1&gt;
&lt;p&gt;During a Purple Team engagement, we had to list and rank risky components across the network. One of them caught our attention: Cato Client, a VPN client program. It was installed everywhere. It runs privileged services. It talks to a GUI. It handles network configuration. From an attacker perspective, this is exactly the kind of software you want to understand. However, saying "this looks risky" is not enough. There is nothing better than &lt;code&gt;PoC||GTFO&lt;/code&gt;. So the question was simple: can we find a real bug and turn it into something useful? This is how it started. And then &lt;a href="https://blog.quarkslab.com/author/yv.html"&gt;my teammate&lt;/a&gt; sent me this message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;💬 YV: 
&lt;em&gt;"I have a new target for you. It has everything you like: named pipes, protobuf and .NET."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He knows me well. That sounds like a fun challenge.&lt;/p&gt;
&lt;p&gt;This writeup covers a local privilege escalation in Cato Client. The bug starts in the split-tunnel upload feature, goes through a named pipe, and ends up with a &lt;code&gt;SYSTEM&lt;/code&gt; shell.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;🧨 &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-10739"&gt;CVE-2026-10739&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Cato Networks SDP Client for Windows is vulnerable to Local Privilege Escalation.&lt;/p&gt;
&lt;p&gt;Confirmed on versions &lt;code&gt;6.2.0&lt;/code&gt; and &lt;code&gt;6.4.6&lt;/code&gt; on Windows. From vendor: &lt;code&gt;any version before 6.12.6&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="cato-split-tunnel"&gt;Cato &amp;amp; Split Tunnel&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://www.catonetworks.com/"&gt;Cato Client&lt;/a&gt; is the endpoint client used to connect users to Cato Cloud.&lt;/p&gt;
&lt;p&gt;One feature is split tunneling. &lt;a href="https://support.catonetworks.com/hc/en-us/articles/16007802440349-Routing-with-the-Cato-Client-Split-Tunnel-Policy"&gt;Cato documentation&lt;/a&gt; explains that administrators can let users upload a text file to decide which IP ranges are included or excluded from the encrypted tunnel. The file contains an &lt;code&gt;include&lt;/code&gt; or &lt;code&gt;exclude&lt;/code&gt; mode, followed by IP ranges and masks.&lt;/p&gt;
&lt;p&gt;In the client UI, this feature appears as a local upload for the split tunnel configuration.&lt;/p&gt;
&lt;p class="center-text"&gt;
&lt;img src="resources/2026-10-01_cato-client-lpe/settings-menu-with-ccst-feature.png"/&gt;&lt;br/&gt;
&lt;i&gt;Cato Client settings - split tunnel file upload.&lt;/i&gt;
&lt;/p&gt;
&lt;p&gt;A minimal valid &lt;code&gt;.ccst&lt;/code&gt; file looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;include
10.10.10.0/24
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can find more information about the split tunnel &lt;a href="https://support.catonetworks.com/hc/en-us/articles/16007802440349-Routing-with-the-Cato-Client-Split-Tunnel-Policy"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h1 id="walkthrough"&gt;Walkthrough&lt;/h1&gt;
&lt;h2 id="discovery"&gt;Discovery&lt;/h2&gt;
&lt;p&gt;The first useful observation came from a normal upload.&lt;/p&gt;
&lt;p&gt;When a valid CCST file is uploaded, the client accepts it and the (privileged) backend creates split-tunnel state (&lt;code&gt;.stp&lt;/code&gt;) under &lt;code&gt;ProgramData&lt;/code&gt;.&lt;/p&gt;
&lt;p class="center-text"&gt;
&lt;img src="resources/2026-10-01_cato-client-lpe/good-ccst-upload.png"/&gt;&lt;br/&gt;
&lt;i&gt;Valid CCST upload.&lt;/i&gt;
&lt;/p&gt;
&lt;p&gt;So I tried the opposite: upload a file with invalid CCST content.&lt;/p&gt;
&lt;p&gt;The parser rejects it. That part is normal. But the cleanup path is more interesting: the generated split-tunnel file is deleted.&lt;/p&gt;
&lt;p class="center-text"&gt;
&lt;img src="resources/2026-10-01_cato-client-lpe/bad-ccst-upload.png"/&gt;&lt;br/&gt;
&lt;i&gt;Invalid CCST upload. The generated file is cleaned up.&lt;/i&gt;
&lt;/p&gt;
&lt;p&gt;At this point, it is not yet a vulnerability. A privileged process deleting its own temporary file is not enough. Moreover, the destination directory has sane permissions. A limited user cannot just drop or replace things there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;PS &lt;/span&gt;&lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:\&lt;/span&gt;&lt;span class="n"&gt;ProgramData&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;CatoNetworks&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;SDPClient&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;icacls&lt;/span&gt; &lt;span class="p"&gt;.\&lt;/span&gt;&lt;span class="n"&gt;ST&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;
&lt;span class="p"&gt;.\&lt;/span&gt;&lt;span class="n"&gt;ST&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt; &lt;span class="n"&gt;AUTORITE&lt;/span&gt; &lt;span class="n"&gt;NT&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Syst&amp;egrave;me&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;AUTORITE&lt;/span&gt; &lt;span class="n"&gt;NT&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Syst&amp;egrave;me&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;WDAC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;WO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;GR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;DC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;BUILTIN&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Administrateurs&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;BUILTIN&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Administrateurs&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;WDAC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;WO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;GR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;DC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;BUILTIN&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Utilisateurs&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;BUILTIN&lt;/span&gt;&lt;span class="p"&gt;\&lt;/span&gt;&lt;span class="n"&gt;Utilisateurs&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;OI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;CI&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;GR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As there was no easy win in the filesystem permissions, the primitive was not "just write a symlink in ProgramData and win".&lt;/p&gt;
&lt;p&gt;The next question was: how does the low-privileged GUI ask the &lt;code&gt;SYSTEM&lt;/code&gt; process to parse and delete those files?&lt;/p&gt;
&lt;h2 id="named-pipe-time"&gt;Named pipe time&lt;/h2&gt;
&lt;p&gt;Our internal named pipe tool showed the answer: local IPC over a named pipe.&lt;/p&gt;
&lt;p class="center-text"&gt;
&lt;img src="resources/2026-10-01_cato-client-lpe/named-pipe-usage-set-ccst-file.png"/&gt;&lt;br/&gt;
&lt;i&gt;Cato GUI talks to the service through a named pipe.&lt;/i&gt;
&lt;/p&gt;
&lt;p&gt;The pipe is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;\\.\pipe\cato-VPN
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The service-side process observed during the proof was:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;C:\Program Files (x86)\Cato Networks\Cato Client\winvpnclient.cli.exe
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;and it runs as:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;NT AUTHORITY\SYSTEM
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Nice. A low-privileged process asks a &lt;code&gt;SYSTEM&lt;/code&gt; process to parse a local file and delete cleanup artifacts.&lt;/p&gt;
&lt;p&gt;But direct access failed. We couldn't interact directly with the pipe. Sending commands from a random process to the pipe did not work.&lt;/p&gt;
&lt;p&gt;With the help of Ghidra, we can see the named-pipe server checks the process talking to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;GetNamedPipeClientProcessId&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="n"&gt;GetNamedPipeClientSessionId&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The interesting part is what happens in the client-connected callback after that.&lt;/p&gt;
&lt;p&gt;The callback first checks if IPC certificate validation is enabled. If yes, it resolves the executable path from the connecting PID, then verifies the executable certificate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;cVar2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FUN_1401e5f20&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// certificate check enabled?&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cVar2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;FUN_140767200&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;client_pid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;cVar2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;thunk_FUN_140753b80&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;expected_cert&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cVar2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="s"&gt;"%s: Failed to register client. Could not verify client certificate. Close connection."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;FUN_14076fc50&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// close pipe&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;FUN_140767200&lt;/code&gt; is the part resolving the client image path:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;hProcess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OpenProcess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;client_pid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;QueryFullProcessImageNameW&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hProcess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;local_248&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;local_294&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The verification helper then parses the executable signature and compares certificate material:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;FUN_1407589d0&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="n"&gt;FUN_140758aa0&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;iVar2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;memcmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Buf1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;expected_cert_material&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Conclusion: a random process does not pass this check. The pipe connection needs to originate from a process image signed with the expected Cato certificate, unless certificate checking is disabled by configuration.&lt;/p&gt;
&lt;h2 id="bypassing-the-certificate-check"&gt;Bypassing the certificate check&lt;/h2&gt;
&lt;p&gt;This part seemed familiar. In the &lt;a href="https://blog.quarkslab.com/k7-antivirus-named-pipe-abuse-registry-manipulation-and-privilege-escalation.html"&gt;K7 writeup&lt;/a&gt;, the patch also tried to block random clients from talking to a privileged named pipe. Manual mapping a payload into a signed/trusted process was enough to get back inside the IPC path.&lt;/p&gt;
&lt;p&gt;Same idea here.&lt;/p&gt;
&lt;p&gt;The PoC starts a signed Cato process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;C:\Program Files (x86)\Cato Networks\Cato Client\CatoClient.exe
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then it manually maps a DLL payload into it. The DLL connects to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;\\.\pipe\cato-VPN
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From the service point of view, the connection comes from a Cato-signed executable, so the certificate check is not a problem anymore.&lt;/p&gt;
&lt;h2 id="protobuf-you-said"&gt;Protobuf, you said?&lt;/h2&gt;
&lt;p&gt;The boring part was rebuilding the Protobuf messages required to talk to the service correctly. This is where AI was helpful: not to find the bug, but to automate repetitive message-building work once the protocol fields were known.&lt;/p&gt;
&lt;p&gt;The useful part came from the decompiled .NET client/common code. The Cato GUI ships generated Google.Protobuf classes under &lt;code&gt;CatoCommon&lt;/code&gt;, and the service communication wrapper shows how the real client builds messages.&lt;/p&gt;
&lt;p&gt;From &lt;code&gt;ServiceCommunication.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sendUiRegisterCommand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ClientToService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;clientToService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreateBasicMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClientToService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UiRegister&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;clientToService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UiRegister&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C2sUiRegister&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;UiProcessId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_processId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;UiSessionId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;UserSidString&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_userSidString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;AadUserUpn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;_aadUserUpn&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;SendCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientToService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;UploadSplitTunnelFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ClientToService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;clientToService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CreateBasicMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ClientToService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SplitTunnelUpload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;clientToService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UploadStFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;C2sUploadSplitTunnelFile&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Filepath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;enabled&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;SendCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientToService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The generated protobuf classes then give the exact field layout.&lt;/p&gt;
&lt;p&gt;From &lt;code&gt;C2sUiRegister.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UiProcessIdFieldNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UiSessionIdFieldNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserSidStringFieldNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From &lt;code&gt;C2sUploadSplitTunnelFile.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EnabledFieldNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FilepathFieldNumber&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And &lt;code&gt;ClientToService&lt;/code&gt; gives the oneof body fields and command IDs used by the PoC.&lt;/p&gt;
&lt;p&gt;From &lt;code&gt;ClientToService.cs&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BodyOneofCase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;UiRegister&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;UploadStFile&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;37&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;EnableLocalSt&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;38&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Commands&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;UiRegister&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;SplitTunnelUpload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;49&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;LocalSplitTunnelEnable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So the actual work was mostly: extract the protobuf structure from the .NET code, identify the command IDs and body fields, then rebuild only those messages in the injected payload. The C payload source code is available here: &lt;a href="resources/2026-10-01_cato-client-lpe/CatoPipePayload.c"&gt;CatoPipePayload.c&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="the-bug"&gt;The bug&lt;/h2&gt;
&lt;p&gt;After solving the pipe connection issue, the real bug is simple.&lt;/p&gt;
&lt;p&gt;As seen before, the &lt;code&gt;.stp&lt;/code&gt; file is built using a user SID. However, this value is not derived from the real client token. It can be manipulated by the user inside the named pipe message. The problem: this client-supplied SID is used as part of a filesystem path.&lt;/p&gt;
&lt;p&gt;The generated split-tunnel output path looks like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&amp;lt;split-tunnel-storage&amp;gt;\ccst_&amp;lt;client supplied SID&amp;gt;.stp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That means the SID is treated as two things at the same time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;identity material;&lt;/li&gt;
&lt;li&gt;a safe filename component.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It is attacker-controlled, and &lt;code&gt;..\\&lt;/code&gt; path components are not rejected.&lt;/p&gt;
&lt;p&gt;So instead of a real SID, the payload can register with something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;\\..\\..\\..\\..\\..\\..\\tmp\\foobar
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The service then builds a path that still starts in the split-tunnel directory, but Windows normalizes the traversal and the final write reaches:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;C:\tmp\foobar.stp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At this stage we have two related primitives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;with a valid CCST file: arbitrary file write as SYSTEM, with a forced &lt;code&gt;.stp&lt;/code&gt; suffix ;&lt;/li&gt;
&lt;li&gt;with an invalid CCST file: delete of the generated &lt;code&gt;.stp&lt;/code&gt; path as &lt;code&gt;SYSTEM&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, the delete is the fun one.&lt;/p&gt;
&lt;h2 id="from-delete-to-system"&gt;From delete to SYSTEM&lt;/h2&gt;
&lt;p&gt;The cleanup path is reached when the uploaded CCST file is readable but invalid. To summarize, as a low-privileged user, the flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;signed CatoClient.exe process origin&lt;/li&gt;
&lt;li&gt;cato-VPN named pipe certificate gate passes&lt;/li&gt;
&lt;li&gt;UiRegister with attacker-controlled UserSidString&lt;/li&gt;
&lt;li&gt;UserSidString reused in ccst_&lt;sid&gt;.stp&lt;/sid&gt;&lt;/li&gt;
&lt;li&gt;path traversal escapes the split-tunnel directory&lt;/li&gt;
&lt;li&gt;SplitTunnelUpload points to an invalid CCST file&lt;/li&gt;
&lt;li&gt;parser fails&lt;/li&gt;
&lt;li&gt;cleanup deletes the generated .stp path as SYSTEM&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The last "problem" &lt;em&gt;(which is not really a problem)&lt;/em&gt; is that we control the path for the delete operation, but the generated target has to end with &lt;code&gt;.stp&lt;/code&gt;. Solution? Symlink, of course. We only need to redirect this privileged delete to something useful.&lt;/p&gt;
&lt;p&gt;The PoC uses the well-known &lt;code&gt;C:\Config.Msi&lt;/code&gt; Windows Installer rollback technique. The idea is &lt;a href="https://www.zerodayinitiative.com/blog/2022/3/16/abusing-arbitrary-file-deletes-to-escalate-privilege-and-other-great-tricks"&gt;documented by ZDI&lt;/a&gt; and was also used in previous &lt;a href="https://blog.quarkslab.com/avira-deserialize-delete-and-escalate-the-proper-way-to-use-an-av.html#CVE-2026-27748"&gt;file&lt;/a&gt;/&lt;a href="https://blog.quarkslab.com/avira-deserialize-delete-and-escalate-the-proper-way-to-use-an-av.html#CVE-2026-27750"&gt;folder&lt;/a&gt; delete LPE chains.&lt;/p&gt;
&lt;p&gt;The Cato-specific redirection is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;C:\poc -&amp;gt; \RPC Control
\RPC Control\deleteme.stp -&amp;gt; \??\C:\Config.Msi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then the malicious SID points the generated &lt;code&gt;.stp&lt;/code&gt; path to:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;C:\poc\deleteme.stp
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When Cato cleanup deletes that path as &lt;code&gt;SYSTEM&lt;/code&gt;, it reaches:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;C:\Config.Msi
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Procmon evidence showed:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Procmon Config.Msi delete" src="resources/2026-10-01_cato-client-lpe/procmon-config.msi-delete.png"/&gt;&lt;/p&gt;
&lt;p&gt;After that, the Windows Installer rollback chain does the rest and gives code execution as &lt;code&gt;SYSTEM&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-autonomous-proof-of-concept"&gt;The autonomous proof of concept&lt;/h2&gt;
&lt;p&gt;The final PoC is an autonomous wrapper:&lt;/p&gt;
&lt;p&gt;It embeds:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the MSI rollback payload;&lt;/li&gt;
&lt;li&gt;the rollback files;&lt;/li&gt;
&lt;li&gt;the Cato pipe payload DLL.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Expected flow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Prepare the Windows Installer rollback state under &lt;code&gt;C:\Config.Msi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create the &lt;code&gt;C:\poc\deleteme.stp -&amp;gt; C:\Config.Msi&lt;/code&gt; redirection.&lt;/li&gt;
&lt;li&gt;Manual-map the embedded Cato pipe DLL into signed &lt;code&gt;CatoClient.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Trigger split-tunnel upload cleanup with invalid CCST input.&lt;/li&gt;
&lt;li&gt;Wait for &lt;code&gt;C:\Config.Msi&lt;/code&gt; to be deleted by &lt;code&gt;winvpnclient.cli.exe&lt;/code&gt; as &lt;code&gt;SYSTEM&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the second MSI stage and launch the configured command.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;And the result:&lt;/p&gt;
&lt;p class="center-text"&gt;
&lt;img src="resources/2026-10-01_cato-client-lpe/CatoEOP.gif"/&gt;&lt;br/&gt;
&lt;i&gt;First LPE PoC.&lt;/i&gt;
&lt;/p&gt;
&lt;p class="center-text"&gt;
&lt;img src="resources/2026-10-01_cato-client-lpe/catoeop-proof.png"/&gt;&lt;br/&gt;
&lt;i&gt;SYSTEM shell after the Cato delete and MSI rollback chain.&lt;/i&gt;
&lt;/p&gt;
&lt;p&gt;Full video with a self-contained binary: &lt;a href="resources/2026-10-01_cato-client-lpe/CatoEOP.mp4"&gt;CatoEOP.mp4&lt;/a&gt;&lt;/p&gt;
&lt;h1 id="conclusion_1"&gt;Conclusion&lt;/h1&gt;
&lt;p&gt;This one was fun because it was not a single obvious bug. The service did have a client check. The directory permissions were not broken. The cleanup delete only happens on invalid &lt;code&gt;.ccst&lt;/code&gt; file upload. The named pipe was not open to everyone. But the chain was there:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;signed process origin&lt;/li&gt;
&lt;li&gt;SID path traversal over protobuf&lt;/li&gt;
&lt;li&gt;invalid CCST cleanup&lt;/li&gt;
&lt;li&gt;SYSTEM delete operation&lt;/li&gt;
&lt;li&gt;Symlink redirection&lt;/li&gt;
&lt;li&gt;SYSTEM shell&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This is exactly why I like turning risk assessment into exploitation. &lt;em&gt;"This software is installed everywhere and runs privileged code"&lt;/em&gt; is a useful warning. But a working 0day LPE demonstration is much better. It proves the risk is not only a line in a report. I also have to say a word about AI in this one. It wasn't useful to find the bug, but it saved me a lot of time, probably a few days, to build a working autonomous PoC based on previous work.&lt;/p&gt;
&lt;h1 id="disclosure-timeline"&gt;Disclosure timeline&lt;/h1&gt;
&lt;p&gt;Below we include a timeline of all the relevant events during the coordinated vulnerability disclosure process with the intent of providing transparency to the whole process and our actions.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2026-06-02&lt;/strong&gt;: Quarkslab &lt;a href=""&gt;reported the vulnerability to Cato Networks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-06-03&lt;/strong&gt;: The vendor acknowledged our report.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-06-15&lt;/strong&gt;: Quarkslab requested an update, the vendor replied on the same day that the vulnerability was pending remediation by the engineering team.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-08-06&lt;/strong&gt;: Quarkslab requested a tentative timeline for remediation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-08-06&lt;/strong&gt;: The vendor informed that the vulnerability was already fixed internally and the rollout was expected at the end of August. Asked Quarkslab to hold off disclosure until then.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-09-16&lt;/strong&gt;: Quarkslab asked for a status update and a clear release date too coordinate disclosure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-09-29&lt;/strong&gt;: Quarkslab asked the vendor if a CVE was assigned to the vulnerability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-09-30&lt;/strong&gt;: The vendor informed that &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-10739"&gt;CVE-2026-10739&lt;/a&gt; was assigned to the vulnerability.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-09-30&lt;/strong&gt;: The vendor published a &lt;a href="https://knowledge.catonetworks.com/docs/cve-2026-10726-cve-2026-10739-impacts-windows-client-versions-lower-than-6126"&gt;Security Announcement&lt;/a&gt; to customers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2026-10-01&lt;/strong&gt;: This blog post is published.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="references"&gt;References&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;Cve.org - &lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-10739"&gt;CVE-2026-10739&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cato Networks - &lt;a href="https://knowledge.catonetworks.com/docs/cve-2026-10726-cve-2026-10739-impacts-windows-client-versions-lower-than-6126"&gt;CVE-2026-10726 &amp;amp; CVE-2026-10739 that Impacts Windows Client Versions Lower than 6.12.6&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Cato Networks - &lt;a href="https://support.catonetworks.com/hc/en-us/articles/16007802440349-Routing-with-the-Cato-Client-Split-Tunnel-Policy"&gt;Routing with the Cato Client / Split Tunnel Policy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;GitHub - &lt;a href="https://github.com/Wh04m1001/CVE-2025-48799"&gt;CVE-2025-48799&lt;/a&gt; (used as a main reference for PoC automation, thanks!)&lt;/li&gt;
&lt;li&gt;PoC payload source - &lt;a href="resources/2026-10-01_cato-client-lpe/CatoPipePayload.c"&gt;CatoPipePayload.c&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Quarkslab - &lt;a href="https://blog.quarkslab.com/k7-antivirus-named-pipe-abuse-registry-manipulation-and-privilege-escalation.html"&gt;K7 Antivirus: Named pipe abuse, registry manipulation and privilege escalation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Quarkslab - &lt;a href="https://blog.quarkslab.com/avira-deserialize-delete-and-escalate-the-proper-way-to-use-an-av.html"&gt;Avira: Deserialize, Delete and Escalate - The Proper Way to Use an AV &lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ZDI PoC - &lt;a href="https://github.com/thezdi/PoC/blob/main/FilesystemEoPs/FolderOrFileDeleteToSystem/"&gt;FilesystemEoPs / FolderOrFileDeleteToSystem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;ZDI - &lt;a href="https://www.zerodayinitiative.com/blog/2022/3/16/abusing-arbitrary-file-deletes-to-escalate-privilege-and-other-great-tricks"&gt;Abusing Arbitrary File Deletes to Escalate Privilege&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</content><category term="Vulnerability"></category><category term="2026"></category><category term="windows"></category><category term="pentest"></category><category term="vulnerability"></category><category term="vpn"></category><category term="named-pipe"></category><category term="Cato Networks"></category><category term="exploit"></category><category term="LPE"></category><category term="CVE-2026-10739"></category></entry></feed>