← Back to Posts

ClickOnce Abuse

How do I use this?

Click this link Copy init KQL. This will copy a lot of KQL to clipboard that will create the required tables, ASIM parsers, helper functions, and start ingestion of the telemetry. A new window will also open to Azure Data Explorer. Create a free cluster and a database (and a Microsoft account if you don't have one) then paste the clipboard contents into the query window and click Run. The bootstrap process will take about a minute. If you have an existing ADX cluster with data in it, keep in mind that the bootstrap process wipes functions and tables that share names with many common tables. You should create another database if you're concerned about data loss.

The Copy init KQL functionality depends on JS. If it isn't enabled, the link will open a new window to GitHub where you can select all and copy the KQL. Then visit Azure Data Explorer to paste and run the bootstrap query. There are links under the Actions heading on the right for convenience.

Synopsis

This simple attack simulation executes ClickOnceBlobber by dazzyddos, a proof-of-concept tool that weaponizes signed .NET ClickOnce applications for initial access through AppDomainManager injection and DLL hijacking. Defenders may want to monitor the dfsvc.exe process and the ClickOnce application cache at %LOCALAPPDATA%\Apps\2.0\. Pentest Laboratories has an excellent article covering the mechanics of this named AppDomainManager Injection and Detection. Note that this article is now nearly six years old and is primarily useful for understanding the underlying injection technique. Also, Sysmon now handles .NET assembly load telemetry differently and there are better detections available.

The MITRE technique page and its references provide essential background; if you're unfamiliar with the technique, I highly recommend reading T1127.002 Trusted Developer Utilities Proxy Execution: ClickOnce and the Pentest Laboratories article.

Two recent campaigns demonstrate active exploitation. The OneClik exercise documented by Trellix in mid 2025 targeted the energy sector using ClickOnce manifests hosted on Azure with AppDomainManager hijacking and Golang backdoors. This is the same injection technique that ClickOnceBlobber implements. In March 2026, Arctic Wolf reported that the threat actor SloppyLemming delivered ClickOnce manifests via spearphishing PDFs against government targets. See their article SloppyLemming Deploys BurrowShell and Rust-Based RAT to Target Pakistan and Bangladesh.

Preparation

I first attempted this simulation with the Kusto.Explorer ClickOnce application, but I was unable to proceed due to more stringent validation of Microsoft-signed applications. I instead grabbed a signed ClickOnce application named TestClickOnce from the website ClickOnce Get. See the GitHub page for ClickOnceBlobber to see detailed instructions on building and deployment.

Analysis

First, lets identify process execution:

DeviceProcessEvents
| where InitiatingProcessFileName =~ "dfsvc.exe"
| project
	Timestamp, DeviceName, FileName, FolderPath, ProcessCommandLine, InitiatingProcessFileName,
    InitiatingProcessFolderPath, InitiatingProcessCommandLine, InitiatingProcessParentFileName

Output:

TimestampDeviceNameFileNameFolderPathProcessCommandLineInitiatingProcessFileNameInitiatingProcessFolderPathInitiatingProcessCommandLineInitiatingProcessParentFileName
2026-03-06 17:54:59.335Zjd-win11-22h2-1.ludus.domaindfsvc.exeC:\Windows\Microsoft.NET\Framework64\v4.0.30319\dfsvc.exe"dfsvc.exe"rundll32.exec:\windows\system32\rundll32.exe"rundll32.exe" "C:\Windows\System32\dfshim.dll",ShOpenVerbApplication http://10.2.20.169:8080/TestClickOnce.applicationmsedge.exe
2026-03-06 17:55:10.954Zjd-win11-22h2-1.ludus.domainTestClickOnce.exeC:\Users\domainuser\AppData\Local\Apps\2.0\PZ132NJ0.HTL\ZK0RZMJV.YEM\test..tion_0000000000000000_0001.0000_d41ad6f01d939..."TestClickOnce.exe"dfsvc.exec:\windows\microsoft.net\framework64\v4.0.30319\dfsvc.exe"dfsvc.exe"rundll32.exe

Looks like the user ran the ClickOnce application with Edge. Note the FolderPath. There are other known artifacts for ClickOnce deployments. Let's find them:

Corelight_CL
| where log_type == "http"
| extend uri = tostring(ParsedMessage.uri),
         http_host = tostring(ParsedMessage["host"]),
         method = tostring(ParsedMessage.method),
         user_agent = tostring(ParsedMessage.user_agent),
         status_code = toint(ParsedMessage.status_code),
         orig_h = tostring(ParsedMessage["id.orig_h"]),
         resp_h = tostring(ParsedMessage["id.resp_h"])
| where uri has_any (".application", ".manifest", ".deploy", ".exe.config")
| project
    TimeGenerated, method, orig_h, resp_h, uri, status_code, user_agent

Output:

TimeGeneratedmethodorig_hresp_huristatus_codeuser_agent
2026-03-06T17:54:56.075ZGET10.2.10.2110.2.20.169/TestClickOnce.application304Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0
2026-03-06T17:55:01.026ZGET10.2.10.2110.2.20.169/TestClickOnce.application200
2026-03-06T17:55:01.174ZGET10.2.10.2110.2.20.169/TestClickOnce.application200
2026-03-06T17:55:01.200ZGET10.2.10.2110.2.20.169/Application Files/TestClickOnce_1_0_0_0/TestClickOnce.exe.manifest200
2026-03-06T17:55:06.550ZGET10.2.10.2110.2.20.169/Application Files/TestClickOnce_1_0_0_0/TestClickOnceHelper.dll.deploy200
2026-03-06T17:55:06.757ZGET10.2.10.2110.2.20.169/Application Files/TestClickOnce_1_0_0_0/TestClickOnce.pdb.deploy200
2026-03-06T17:55:06.769ZGET10.2.10.2110.2.20.169/Application Files/TestClickOnce_1_0_0_0/TestClickOnce.exe.config.deploy200
2026-03-06T17:55:06.779ZGET10.2.10.2110.2.20.169/Application Files/TestClickOnce_1_0_0_0/TestClickOnce.exe.deploy200

What was loaded by the parent process?

DeviceImageLoadEvents
| where FolderPath has @"\AppData\Local\Apps\2.0\"
| project
    Timestamp, FileName, FolderPath, SHA256, InitiatingProcessFileName,
	InitiatingProcessFolderPath, InitiatingProcessCommandLine

Output:

TimestampFileNameFolderPathSHA256InitiatingProcessFileNameInitiatingProcessCommandLine
2026-03-06T17:55:11.022ZTestClickOnce.exeC:\Users\domainuser\AppData\Local\Apps\2.0...\TestClickOnce.exe4008b8...cc02testclickonce.exe"TestClickOnce.exe"
2026-03-06T17:55:12.251ZTestClickOnceHelper.dllC:\Users\domainuser\AppData\Local\Apps\2.0...\TestClickOnceHelper.dll60e758...cc02testclickonce.exe"TestClickOnce.exe"

Unfortunately, we don't have the FileProfile() function from Defender XDR, where we could get enriched data like SignatureState or GlobalPrevalence. We can look for the signature information with Sysmon Event ID 7, but there's no way to find the number of instances of this file hash globally by other means. Let's do the former now:

WindowsEvent
| where Provider == "Microsoft-Windows-Sysmon" and EventID == 7
| extend Image = tostring(EventData.Image),
         ImageLoaded = tostring(EventData.ImageLoaded),
         Signed = tostring(EventData.Signed),
         SignatureStatus = tostring(EventData.SignatureStatus),
         Hashes = tostring(EventData.Hashes)
| where Image endswith "TestClickOnce.exe"
| where ImageLoaded has @"\AppData\Local\Apps\2.0\"
| project
    TimeGenerated, Computer, ImageLoaded, Signed, SignatureStatus, Hashes

Output:

TimeGeneratedComputerImageLoadedSignedSignatureStatusSHA256IMPHASH
2026-03-06T17:55:11.053ZJD-WIN11-22H2-1.ludus.domain...\TestClickOnce.exefalseUnavailable4008B8...CB9E10F34D5F...A744
2026-03-06T17:55:12.299ZJD-WIN11-22H2-1.ludus.domain...\TestClickOnceHelper.dllfalseUnavailable60E758...CBCC02DAE02F...42DAA
2026-03-06T17:55:12.300ZJD-WIN11-22H2-1.ludus.domain...\TestClickOnceHelper.dllfalseUnavailable60E758...CBCC02DAE02F...42DAA

Sysmon couldn't find a valid Authenticode signature for our ProxyBlob Agent DLL, TestClickOnceHelper.dll. The query limited results to images loaded from the ClickOnce cache, but you can also look at the .NET/CLR assembly loads if you remove the line | where ImageLoaded has @"\AppData\Local\Apps\2.0\".

Separately, note that a ClickOnce application can carry a manifest-signing certificate while the underlying binaries can have their own Authenticode signatures. ClickOnceBlobber rebuilds the deployment package to inject the malicious TestClickOnceHelper.dll and .exe.config file that enables AppDomainManager hijacking. This process replaces the original manifest signature with a new one. The website where I obtained this ClickOnce app publishes the manifest certificate.

Moving on, while we don't have GlobalPrevalence from FileProfile(), we can still baseline the use of ClickOnce and ClickOnce applications in the environment.

let ClickOnceDeployments = 
    DeviceProcessEvents
    | where InitiatingProcessFileName =~ "dfsvc.exe"
    | where FolderPath has @"\AppData\Local\Apps\2.0\"
    | summarize 
        DeployedApps = make_set(FileName, 10),
        DeployedPaths = make_set(FolderPath, 10),
        DeployedHashes = make_set(SHA256, 10),
        DeployedCommandLines = make_set(ProcessCommandLine, 10),
        FirstDeployed = min(Timestamp),
        LastDeployed = max(Timestamp),
        DeploymentCount = count()
        by DeviceName;
let DfsvcExecutions =
    DeviceProcessEvents
    | where FileName =~ "dfsvc.exe"
    | summarize 
        ExecutionCount = count(),
        FirstSeen = min(Timestamp),
        LastSeen = max(Timestamp),
        ParentProcesses = make_set(InitiatingProcessFileName, 10),
        ParentCommandLines = make_set(InitiatingProcessCommandLine, 10),
        DfsvcPaths = make_set(FolderPath, 10),
        DfsvcCommandLines = make_set(ProcessCommandLine, 10)
        by DeviceName;
DfsvcExecutions
| join kind=leftouter ClickOnceDeployments on DeviceName
| project 
    DeviceName, ParentProcesses, ParentCommandLines, DfsvcPaths, DfsvcCommandLines,
    ExecutionCount, FirstSeen, LastSeen, DeployedApps, DeployedHashes,
    DeployedCommandLines, DeploymentCount, FirstDeployed, LastDeployed

Output:

DeviceNameExecutionCountFirstSeenLastSeenParentProcessesDeploymentCountDeployedAppsDeployedHashesDeployedCommandLinesFirstDeployedLastDeployed
jd-win11-22h2-1.ludus.domain12026-03-06T17:54:59.335Z2026-03-06T17:54:59.335Z["rundll32.exe"]1["TestClickOnce.exe"]["4008b8cdc907ed469b5f044c80ad808f9797e0f4e5d3ff3b38850372d2cb9e10"]["TestClickOnce.exe"]2026-03-06T17:55:10.955Z2026-03-06T17:55:10.955Z

Since this is a lab env, these are predictable results.


To confirm all of the files that were created on disk:

DeviceFileEvents
| where DeviceName == "jd-win11-22h2-1.ludus.domain"
| where FolderPath has @"\AppData\Local\Apps\2.0\"
| project 
    Timestamp, ActionType, FileName, FolderPath,
    InitiatingProcessFileName, InitiatingProcessCommandLine, SHA256

Output:

TimestampActionTypeFileNameFolderPathInitiatingProcessFileNameInitiatingProcessCommandLineSHA256
2026-03-06T17:55:07.433ZFileCreatedTestClickOnceHelper.dllC:\Users\domainuser\AppData\Local\Apps\2.0\PZ132NJ0.HTL\ZK0RZMJV.YEM\test...exe_0000000000000000_0001.0000_none_bf35979fafd71f40\TestClickOnceHelper.dlldfsvc.exe"dfsvc.exe"60e7585f6b1e40dad7373032073918ccf91d4d2a7cbc10c6288fb3e12acbcc02
2026-03-06T17:55:07.448ZFileCreatedTestClickOnce.exe.configC:\Users\domainuser\AppData\Local\Apps\2.0\PZ132NJ0.HTL\ZK0RZMJV.YEM\test...exe_0000000000000000_0001.0000_none_bf35979fafd71f40\TestClickOnce.exe.configdfsvc.exe"dfsvc.exe"d1f60245d2a6f81ecffb77fcb50c632d5cefc3848e2f131d90f1b41b28d9eb55
2026-03-06T17:55:07.484ZFileCreatedTestClickOnce.exeC:\Users\domainuser\AppData\Local\Apps\2.0\PZ132NJ0.HTL\ZK0RZMJV.YEM\test..tion_0000000000000000_0001.0000_d41ad6f01d939c50\TestClickOnce.exedfsvc.exe"dfsvc.exe"4008b8cdc907ed469b5f044c80ad808f9797e0f4e5d3ff3b38850372d2cb9e10

We've deduplicated the results for clarity (there were two results each for the .dll and .exe.config files, possibly due to ClickOnce file staging or MDE's treatment of rename/move operations as FileCreated events).


Did this app phone home?

_ASim_NetworkSession
| where ParentProcessName == "dfsvc.exe"
| where SrcProcessName == "testclickonce.exe"
| project
    TimeGenerated, DvcHostname, User, SrcProcessName, InitiatingProcessFolderPath,
    DstIpAddr, DstPortNumber, DstFQDN, EventResult

Output:

TimeGeneratedDvcHostnameUserSrcProcessNameInitiatingProcessFolderPathDstIpAddrDstPortNumberDstFQDNEventResult
2026-03-06T17:55:14.595Zjd-win11-22h2-1ludus\domainusertestclickonce.exec:\users\domainuser\appdata\local\apps\2.0\pz132nj0.htl\zk0rzmjv.yem\test..d939c50\testclickonce.exe20.209.154.1344439082489234.blob.core.windows.netSuccess

Mehmet Ergene writes in Querying Azure Resource Graph Without Limits Using KQL about creating a whitelist of Azure Storage accounts in your environment for use with threat hunting queries. This would also be very helpful for detecting potential C2 and data exfiltration if you use Azure Storage in your environment. With the help of Suricata, we can catch C2 traffic with an Azure Blob Storage account:

Suricata_CL
| where EventType == "tls"
| where EventData.tls.sni has "blob.core.windows.net"
| project
    TimeGenerated, SrcIp, SrcPort, DestIp, DestPort, EventData.tls.sni

Output:

TimeGeneratedSrcIpSrcPortDestIpDestPortSNI
2026-03-05T18:49:51.300Z10.2.10.215700520.209.154.1344439082489234.blob.core.windows.net

While we're at it, lets aggregate all network connections from ClickOnce cache apps and dfsvc.exe according to MDE:

DeviceNetworkEvents
| where InitiatingProcessFolderPath has @"\AppData\Local\Apps\2.0\" or InitiatingProcessFileName == "dfsvc.exe"
| project
    Timestamp, ActionType, RemoteIP, RemotePort, RemoteUrl,
    InitiatingProcessFileName, InitiatingProcessFolderPath, InitiatingProcessCommandLine, Protocol

Output:

TimestampActionTypeRemoteIPRemotePortRemoteUrlInitiatingProcessFileNameInitiatingProcessFolderPathProtocol
2026-03-06T17:55:01.016ZConnectionSuccess10.2.20.1698080dfsvc.exec:\windows\microsoft.net\framework64\v4.0.30319\dfsvc.exeTcp
2026-03-06T17:55:01.164ZConnectionSuccess10.2.20.1698000dfsvc.exec:\windows\microsoft.net\framework64\v4.0.30319\dfsvc.exeTcp
2026-03-06T17:55:14.595ZConnectionSuccess20.209.154.1344439082489234.blob.core.windows.nettestclickonce.exe...\TestClickOnce.exeTcp

The first two rows show the ClickOnce deployment service dfsvc.exe connecting to the staging server at 10.2.20.169 on ports 8080 and 8000 to retrieve the .application manifest and .deploy payloads. This matches the Zeek HTTP logs from earlier showing the GET requests for the manifest and deploy files.

The last row shows C2 activity where testclickonce.exe reaches out to 9082489234.blob.core.windows.net over port 443.

Detections

If you don't have ClickOnce applications in your environment after baselining (see the analysis for a query), you can check for any unsigned modules loaded by a ClickOnce application. This is based on the Sigma rule Unsigned Module Loaded by ClickOnce Application. This rule currently has a test status in the repo.

WindowsEvent
| where Provider == "Microsoft-Windows-Sysmon" and EventID == 7
| extend Image = tostring(EventData.Image),
         ImageLoaded = tostring(EventData.ImageLoaded),
         Signed = tostring(EventData.Signed),
         SignatureStatus = tostring(EventData.SignatureStatus),
         Hashes = tostring(EventData.Hashes)
| where Image has @"\AppData\Local\Apps\2.0\"
| where Signed == "false" or SignatureStatus == "Expired"
| project
    TimeGenerated, Computer, Image, ImageLoaded, Signed, SignatureStatus, Hashes

There is another Sigma rule, Potentially Suspicious Child Process Of ClickOnce Application. This is also in a test status.

DeviceProcessEvents
| where InitiatingProcessFolderPath has @"\AppData\Local\Apps\2.0\"
| where FileName in~ (
    "calc.exe", "cmd.exe", "cscript.exe", "explorer.exe", "mshta.exe",
    "net.exe", "net1.exe", "nltest.exe", "notepad.exe", "powershell.exe",
    "pwsh.exe", "reg.exe", "regsvr32.exe", "rundll32.exe", "schtasks.exe",
    "werfault.exe", "wscript.exe"
)

You may detect creation of a .exe.config file in the ClickOnce cache following installation:

DeviceFileEvents
| where InitiatingProcessFileName == "dfsvc.exe"
| where FolderPath has @"\AppData\Local\Apps\2.0\"
| where FileName endswith ".exe.config"
| extend RequestAccount = strcat(RequestAccountDomain, "\\", RequestAccountName)
| project
    Timestamp, ActionType, DeviceName, RequestAccount,
    InitiatingProcessFolderPath, FolderPath, FileName

Output:

TimestampActionTypeDeviceNameRequestAccountInitiatingProcessFolderPathFolderPathFileName
2026-03-05T20:30:38.2694273ZFileCreatedjd-win11-22h2-1.ludus.domainludus\domainuserc:\windows\microsoft.net\framework64\v4.0.30319\dfsvc.exeC:\Users\domainuser\AppData\Local\Apps\2.0\68GOJRYE.ZQP\L8QAHERJ.XQ0\test...exe_0000000000000000_0001.0000_none_bf35979fafd71f40\TestClickOnce.exe.configTestClickOnce.exe.config
2026-03-05T20:30:38.2711574ZFileCreatedjd-win11-22h2-1.ludus.domainludus\domainuserc:\windows\microsoft.net\framework64\v4.0.30319\dfsvc.exeC:\Users\domainuser\AppData\Local\Apps\2.0\68GOJRYE.ZQP\L8QAHERJ.XQ0\test..tion_0000000000000000_0001.0000_d41ad6f01d939c50\TestClickOnce.exe.configTestClickOnce.exe.config

This detection catches dfsvc.exe launched by a browser or rundll32.exe with a command line referencing an external URL, which is distinct from more common, legitimate enterprise deployments that reference UNC paths or local file URIs. Combined with the baselining detection from the analysis section, this would cover the vector from the SloppyLemming campaign and our ClickOnceBlobber simulation.

let SuspiciousParents = dynamic([
    "msedge.exe", "chrome.exe", "firefox.exe", "iexplore.exe", "brave.exe",
    "winword.exe", "excel.exe", "powerpnt.exe", "outlook.exe",
    "powershell.exe", "cmd.exe", "mshta.exe", "wscript.exe", "cscript.exe"
]);
_ASim_ProcessCreate
| where TargetProcessName endswith "dfsvc.exe"
| where ActingProcessName in~ (SuspiciousParents)
    or (ActingProcessName =~ "rundll32.exe" 
        and ActingProcessCommandLine has "dfshim.dll" 
        and ActingProcessCommandLine has_any ("http://", "https://"))
| project
    TimeGenerated, Dvc, ActorUsername, ActingProcessName, ActingProcessCommandLine, TargetProcessCommandLine

SloppyLemming doesn't rely solely on AppDomainManager injection via dfsvc.exe. After the ClickOnce manifest delivers the payload, a DLL sideloading package is dropped with a legitimate .NET binary (NGGenTask.exe or phoneactivate.exe) paired with a malicious loader DLL mscorsvc.dll. This happens after ClickOnce deployment and outside of the Apps\2.0 cache. Given this, we could look for a legitimate Microsoft binary loading unsigned DLLs from a user-writable path. This will be very noisy unless your environment is very buttoned up:

let SideloadTargets = dynamic([
    "ngentask.exe", "phoneactivate.exe", "msbuild.exe",
    "installutil.exe", "regsvcs.exe", "regasm.exe"
]);
WindowsEvent
| where Provider == "Microsoft-Windows-Sysmon" and EventID == 7
| extend Image = tostring(EventData.Image),
         ImageLoaded = tostring(EventData.ImageLoaded),
         Signed = tostring(EventData.Signed),
         SignatureStatus = tostring(EventData.SignatureStatus),
         Hashes = tostring(EventData.Hashes)
| where Image has_any (SideloadTargets)
| where ImageLoaded !startswith @"C:\Windows\"
      and ImageLoaded !startswith @"C:\Program Files\"
      and ImageLoaded !startswith @"C:\Program Files (x86)\"
| project
    TimeGenerated, Computer, Image, ImageLoaded, Signed, SignatureStatus, Hashes

We could also look for NGenTask.exe or phoneactivate.exe execution from a user-writable directory:

_ASim_ProcessCreate
| where TargetProcessName in~ ("ngentask.exe", "phoneactivate.exe")
| where TargetProcessCurrentDirectory !startswith @"C:\Windows\"
      and TargetProcessCurrentDirectory !startswith @"C:\Program Files"
| project
    TimeGenerated, Dvc, ActorUsername, TargetProcessName, TargetProcessCommandLine,
    ActingProcessName, ActingProcessCommandLine, TargetProcessCurrentDirectory

Aside from the first detection, I'm not so sure about the quality of these queries overall. ClickOnce applications appear difficult to secure. There are even more gaps when you look at other uses of ClickOnce in the wild:

  • The OneClik exercise by Trellix hides their C2 behind CloudFront, API Gateway, and lambda.
  • SloppyLemming uses more than 100 Cloudflare Workers domains and the BurrowShell traffic is disguised as Windows Update comms.
  • The Acronis research identifies use of ClickOnce runners that connect directly to a ScreenConnect server to fetch components.

I leave further detection engineering as an exercise for the reader. I welcome queries if you'd like to submit something for inclusion (drop me an email). There are many references below if you're interested in more research on ClickOnce abuse.

References

Trellix OneClik: A ClickOnce-Based Red Team Campaign Simulating APT Tactics in Energy Infrastructure
Trellix SideWinder's Shifting Sands: Click Once for Espionage
Arctic Wolf SloppyLemming Deploys BurrowShell and Rust-Based RAT to Target Pakistan and Bangladesh
Acronis Trojanized ScreenConnect installers evolve, dropping multiple RATs on a single machine
Hunt.io AsyncRAT Campaigns Uncovered: How Attackers Abuse ScreenConnect and Open Directories
Cloudflare CloudForce One Unraveling SloppyLemming's Operations Across South Asia
QiAnXin APT-Q-14 Group Combines 0day and ClickOnce Technology to Carry Out Espionage Activities
Pentest Laboratories AppDomainManager Injection and Detection
Mehmet Ergene Querying Azure Resource Graph Without Limits Using KQL
Specter Ops Less SmartScreen More Caffeine: (Ab)Using ClickOnce for Trusted Code Execution
Microsoft ClickOnce Security and Deployment
MITRE Trusted Developer Utilities Proxy Execution: ClickOnce
ClickOnceBlobber
ClickOnceGet
FileProfile() function
Bolthole