Post

The "SharePoint Helper" That Was Really a Localhost Backdoor

A brand-new scheduled task on a two-year-old host launches a fileless PowerShell HTTP-RAT that listens on localhost only and waits for an operator's tunnel.

The "SharePoint Helper" That Was Really a Localhost Backdoor

The views and opinions expressed in this post are my own and do not represent those of my employer. This is a personal blog where I share research and things I’m learning.

TL;DR

A host that had been quietly onboarded for over two years grew a brand-new scheduled task, XblGameCachesTask, that launches C:\ProgramData\sharepoint-assist.ps1 hidden via conhost.exe --headless and -ep bypass. The script is 326KB of string-method obfuscation that folds down to a fileless PowerShell HTTP-RAT: it bypasses AMSI, stands up an HTTP listener on 127.0.0.1:58172, and exposes /run (arbitrary PowerShell) and /window (a generic process memory patcher plus HVNC-style window hiding). What makes it different from a normal beacon: there is no external C2 in the sample at all. It listens on localhost only, so an operator has to reach in through a separate tunnel.

If this is your fleet, do these first:

  • Hunt PowerShell Script Block Logging (Event ID 4104) for HttpListener + RunspaceFactory + VirtualProtectEx in one script, or the literals Global\explorer_wide_thumbcache / 127.0.0.1:58172.
  • Hunt any powershell.exe that owns a LISTENING TCP socket (port 58172, or any) - that is bizarre on a workstation.
  • Alert on Event ID 4698 for the creation of XblGameCachesTask, and 4688 for conhost.exe --headless.

Full indicators and a YARA rule are at the bottom.

A scheduled task that shouldn’t exist

Here’s the thing that made me sit up: the host had been onboarded for over two years, humming along, and then a brand-new scheduled task appears called XblGameCachesTask. Sounds like Xbox housekeeping. It isn’t. It runs this:

1
conhost.exe --headless cmd.exe /c powershell -ep bypass -file "C:\ProgramData\sharepoint-assist.ps1"

Three quiet tells in one line. conhost.exe --headless runs the console with no visible window, a genuinely tidy way to launch PowerShell where nobody sees a blue box flash. -ep bypass waves off execution policy. And the payload is a .ps1 sitting in C:\ProgramData wearing a reassuring “sharepoint-assist” costume.

The file itself is 326KB of pure obfuscation: every API name rebuilt at runtime from chains of string methods, wrapped in hundreds of fake if/else branches gated by meaningless arithmetic. So this post is two stories: how that obfuscation comes off without running a line of it, and what was underneath, which turned out to be a fileless remote-control agent that listens on localhost. That last detail changes how you hunt it. Let’s dig in.

Defender quick reference

FieldDetails
Activity typeFileless PowerShell HTTP-RAT (interactive remote control) with HVNC-style window hiding and an in-memory process patcher
Primary artifactsC:\ProgramData\sharepoint-assist.ps1; scheduled task XblGameCachesTask; mutex Global\explorer_wide_thumbcache; localhost listener 127.0.0.1:58172 (routes /run, /window); dynamic assembly DynWin32_1; inline class W32API / FindPattern
VerdictMalicious
ConfidenceHigh (script fully deobfuscated to cleartext)
Key logsPowerShell Operational 4104 (Script Block Logging); Security 4698 / 4688; Sysmon 1 / 3 / 10; AMSI telemetry
ATT&CKT1053.005, T1059.001, T1564.003, T1140, T1562.001, T1620, T1071.001, T1055
First defender actionsDisable/delete the task XblGameCachesTask; kill the powershell.exe holding 127.0.0.1:58172; quarantine the .ps1; then hunt the external tunnel that forwards to port 58172
Detection opportunities4104 for HttpListener + RunspaceFactory + VirtualProtectEx in one script; 4688 for conhost.exe --headless; Sysmon 3 for powershell.exe owning a LISTENING socket; Sysmon 10 for cross-process VM-write; 4698 for the task creation; YARA rule below
False-positive notesNone known. No legitimate Xbox task uses this name or path, and powershell.exe owning a listening socket is not normal on a workstation.

The attack at a glance

  1. Persistence - a scheduled task XblGameCachesTask launches the script hidden via conhost --headless and -ep bypass.
  2. Defence evasion - the script reflectively neuters AMSI before doing anything else.
  3. Setup - a single-instance mutex, then Win32 APIs built in memory (no P/Invoke on disk).
  4. Command and control - an HTTP listener on http://127.0.0.1:58172/ with two routes.
  5. Objective - /run executes arbitrary PowerShell in a runspace; /window patches process memory and hides windows. An operator drives it through a separate tunnel.

How it works

Stage 1 - The obfuscation wall, and how to walk through it

Open the file and you get a faceful of this:

1
$x = [Ref].("40fFW...AssemblykFUP...".Substring(11,100).Insert(53,"DTXh").Remove(36,22).Replace("xFZyAjURjRIZU4k","Ox")...)

Every meaningful token (Assembly, GetTypes, HttpListener, the lot) is assembled at runtime from junk strings via .Substring(), .Insert(), .Remove(), .Replace(), .Trim(). There were 558 Substring calls, 561 Replace, 596 Trim. On top of that, the script is padded with hundreds of if (4900 - 6026 + 696 ... -le ...) blocks whose only job is to assign dead variables and bloat the file.

The important point for anyone facing this: you don’t decode it by running it. Those string-method chains are pure functions - "abc".Substring(1,2) always returns "bc", no side effects. So the approach is a small constant-folder in Python that parses and evaluates only those pure expressions (string methods, -join/-split, arithmetic) and rewrites each in place with its result. None of the script’s own logic - no reflection, no listener, no sinks - ever executes. It’s the same idea as an AST deobfuscator for obfuscated JavaScript: fold the constants, leave the behaviour as text.

1
2
3
4
# before: 326 KB, ~99% noise
$OmsbzetmVNJbc = [Ref].("40fFW...".Substring(11,100).Insert(53,"DTXh")...)
# after folding:
$asm = [Ref].Assembly.GetTypes()

That one move took the file from 326KB to 88KB and resolved about 99% of the obfuscation. The last 1% needed the modulo operator added to the evaluator - a handful of indexes used %. Once that was in, the opaque if predicates collapsed to plain $true/$false and the dead branches just fell away. From there it reads like normal PowerShell.

Stage 2 - Get comfortable: AMSI off, Win32 in memory

First thing the cleartext does is blind the Antimalware Scan Interface. It walks every loaded type, finds AmsiUtils, and tampers with its private static amsiContext field by reflection:

1
2
3
4
5
6
foreach ($t in [Ref].Assembly.GetTypes()) {
    if ($t.Name -notlike 'A*Utils') { continue }          # AmsiUtils
    foreach ($f in $t.GetFields('NonPublic,Static')) {
        if ($f.Name -like '*Context') { $f.GetValue($null) ... }   # amsiContext
    }
}

Then a single-instance mutex named Global\explorer_wide_thumbcache (picked to blend in with Windows thumbnail-cache internals), and the neat bit: it builds its Win32 access two ways that both avoid a clean disk artefact. A Reflection.Emit dynamic assembly called DynWin32_1, and an inline Add-Type C# class W32API that includes a byte-pattern scanner:

1
2
3
public static List<int> FindPattern(byte[] data, byte[] pattern, bool[] mask, int patternLength) {
    // classic signature scan with wildcard mask - used to locate bytes to patch
}

Hold that thought - a pattern scanner plus VirtualProtectEx is a memory-patching kit.

Stage 3 - A C2 server that listens on localhost

The payload stands up a web server. On the loopback address.

1
2
3
4
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add('http://127.0.0.1:58172/')
$listener.Start()
$resp.Headers.Add('Access-Control-Allow-Origin','*')   # wide-open CORS

Two routes. /run is exactly what it sounds like, arbitrary PowerShell execution:

1
2
3
4
5
6
'/run' {
    $rs = [RunspaceFactory]::CreateRunspace(); $rs.Open()
    $ps = [PowerShell]::Create(); $ps.Runspace = $rs
    $ps.AddScript($body.command) | Out-Null
    $h = $ps.BeginInvoke(); if ($h.AsyncWaitHandle.WaitOne(60000)) { $out = $ps.EndInvoke($h) }
}

And /window is the dangerous one. It takes JSON params {dll, pattern, patch, process, window, ...} and does two jobs: find a byte pattern inside a dll loaded in a target process, flip permissions with VirtualProtectEx, and overwrite it with patch bytes - a generic, remote, point-anywhere memory patcher (read: neuter AMSI, ETW, or EDR user-mode hooks in any process the operator names). Or, given a window handle, SetParent it under a hidden parent window, SetWindowPos, and ShowWindow(hide) - the hidden-desktop / HVNC trick for making windows disappear.

Now the detail that matters most for defenders: there is no external C2 address anywhere in this script. It binds to 127.0.0.1 only. So how does an operator on the internet reach it? They don’t, directly. They must run a separate tunnel (a reverse proxy, ssh -R, a Cloudflare/ngrok-style tunnel, a companion implant, maybe even a malicious browser extension - the wide-open Access-Control-Allow-Origin: * is consistent with a browser-based panel, though I couldn’t confirm the panel itself from this file alone). The implant is deliberately boring on the network. The interesting half is whatever forwards traffic to port 58172, and that’s a second thing to go find.

Techniques observed (MITRE ATT&CK)

The following techniques have been mapped to MITRE ATT&CK for future reference.

TacticTechniqueATT&CK IDWhat it did here
PersistenceScheduled TaskT1053.005XblGameCachesTask launches the script
Defense EvasionHidden windowT1564.003conhost.exe --headless
ExecutionPowerShell, -ep bypassT1059.001scheduled-task command line; /run runspace
Defense EvasionDeobfuscate at runtimeT1140string-method-rebuilt identifiers
Defense EvasionImpair defenses (AMSI)T1562.001AmsiUtils private-field tamper
Defense EvasionReflective in-memory codeT1620Reflection.Emit DynWin32_1, Add-Type
Command & ControlApplication-layer protocol (HTTP)T1071.001HttpListener on 127.0.0.1:58172, /run + /window
Defense EvasionProcess injection / memory patchT1055/window: FindPattern + VirtualProtectEx
Defense EvasionHide artefacts (HVNC)T1564SetParent/ShowWindow window hiding

Why this matters

Strip away the cleverness and this is a hands-on-keyboard backdoor. /run is unrestricted PowerShell execution as whatever account the task runs under; /window can quietly switch off AMSI, ETW, or EDR hooks inside other processes and hide windows from the user sitting at the machine. AMSI is already bypassed before any of that. On a host that’s been stable for two years, the appearance of this task is the loud part of an otherwise very quiet implant.

It’s also a reminder that “no network IOCs in the sample” doesn’t mean “nothing to hunt”. A localhost listener is a design choice that pushes the externally-visible footprint into a separate tunnel, so the absence of a hardcoded domain is itself a clue about how the operator reaches in. I’m not going to put a name to who’s behind it; the tradecraft is solid-but-commodity, and the artefacts below are far more useful than a guess.

What defenders can do

Technique (ATT&CK)What to doEssential EightWhat to hunt for
Arbitrary PowerShell (T1059.001)Constrained Language Mode; app-control .ps1 from C:\ProgramDataUser App Hardening (ML2); Application Control (ML1)Script Block Logging (4104)
Runtime deobfuscation (T1140)CLM disables the .NET reflection these loaders needUser App Hardening (ML2)4104 logs the decoded body
AMSI bypass (T1562.001)CLM + Tamper Protection; block PS2 engineUser App Hardening (ML2)AMSI init failures; 4104
Scheduled task persistence (T1053.005)App-control the payload; restrict task creationApplication Control; Restrict AdminEvent 4698 for XblGameCachesTask
Hidden window (T1564.003)No clean E8 home-4688 for conhost.exe --headless
Localhost HTTP C2 + memory patch (T1071.001 / T1055)No clean E8 home - network architecture-PowerShell owning a LISTEN socket; Sysmon 10 cross-proc access

Constrained Language Mode is the single biggest lever here, and it’s worth dwelling on. Almost every load-bearing trick in this implant - the Reflection.Emit DynWin32_1 assembly, the Add-Type C# compile, the reflective AMSI bypass, even standing up HttpListener through arbitrary .NET - depends on full-language PowerShell. CLM breaks all of it. The Essential Eight model puts this under User Application Hardening at Maturity Level Two, in plain words: “PowerShell is configured to use Constrained Language Mode” and “PowerShell module logging, script block logging and transcription events are centrally logged” (Essential Eight Maturity Model, November 2023). Pair it with Application Control - which at Maturity Level One “restricts the execution of executables, software libraries, scripts, installers, compiled HTML, HTML applications and control panel applets to an organisation-approved set” and is “applied to user profiles and temporary folders” (same document) - and an unapproved .ps1 running out of C:\ProgramData never starts. See Implementing Application Control (November 2023) and Securing PowerShell in the Enterprise (October 2021) for the rollout detail; the latter is the practical guide to CLM without breaking your admins.

Even if you can’t deploy CLM tomorrow, turn on Script Block Logging. This is the quiet hero against obfuscation: Event ID 4104 records the decoded script body that PowerShell actually runs, so all the .Substring().Replace() games are pointless - you see HttpListener, RunspaceFactory, VirtualProtectEx, and the literal Global\explorer_wide_thumbcache and 127.0.0.1:58172 in the clear. It’s the same control that fed half this analysis. Centralised collection (so a local attacker can’t just clear it) is the ML2 expectation.

For the persistence and the hidden launch, app-control still bites the payload regardless of how the task starts it, and restricting who can create scheduled tasks (Restrict Administrative Privileges, Restricting Administrative Privileges, November 2023) limits the foothold; hunt Event ID 4698 for the task creation and 4688 for conhost.exe --headless, which has almost no legitimate use on a normal workstation. For the localhost C2 and the memory patcher, there’s no clean Essential Eight home - this is a monitoring-and-architecture problem. The standout signal is that powershell.exe owns a listening socket, which is bizarre; and the /window patcher means powershell.exe opening cross-process handles into other PIDs (Sysmon Event ID 10). And go find the tunnel forwarding to 58172 - that’s the real ingress.

Hunting and detection summary

  • Script Block Logging (Event ID 4104) - alert on a script containing HttpListener + RunspaceFactory + VirtualProtectEx, or the literals Global\explorer_wide_thumbcache / 127.0.0.1:58172.
  • 4688 / Sysmon 1 - conhost.exe with --headless; powershell.exe -ep bypass -file C:\ProgramData\*.ps1; powershell.exe spawning csc.exe/cvtres.exe (the Add-Type compile).
  • Sysmon 3 / netstat - powershell.exe owning a LISTENING TCP socket (port 58172, or any).
  • Sysmon 10 (ProcessAccess) - powershell.exe opening handles into other processes with VM-write/operation rights.
  • Event ID 4698 - creation of scheduled task XblGameCachesTask.
  • AMSI - scan-initialisation failures around PowerShell start.

Indicators of Compromise

TypeIndicatorNotes
SHA256f1767aaebb55347153c56e21adbf3a41e48663d139279ec8e3b1f1be1db63a53sharepoint-assist.ps1
MD559978abc44e9aa790767baf54122f455same
PathC:\ProgramData\sharepoint-assist.ps1payload location
Scheduled taskXblGameCachesTaskpersistence
MutexGlobal\explorer_wide_thumbcachesingle-instance marker
Listener127.0.0.1:58172 (TCP LISTEN, owned by powershell.exe)local C2
HTTP routes/run, /windowapplication/json; CORS *
Command lineconhost.exe --headless cmd.exe /c powershell -ep bypass -file "C:\ProgramData\sharepoint-assist.ps1"launch chain
Dynamic assemblyDynWin32_1in-memory Win32 (visible in 4104)
Inline C# classW32API / FindPatternsignature scanner for the patcher

No external domains or IPs are baked into the sample - the pivot is the tunnel exposing port 58172.

Detection rules

rule PS_SharePointAssist_LocalHTTP_RAT
{
    meta:
        description = "Fileless PowerShell localhost HTTP-RAT (sharepoint-assist.ps1)"
        author = "Luke Wilkinson"
        date = "2026-06-06"
        sha256 = "f1767aaebb55347153c56e21adbf3a41e48663d139279ec8e3b1f1be1db63a53"
    strings:
        $o1 = ".Substring(" ascii
        $o2 = ".Insert("    ascii
        $o3 = ".TrimEnd("   ascii
        $a1 = "Global\\explorer_wide_thumbcache" ascii wide
        $a2 = "127.0.0.1:58172" ascii wide
        $a3 = "DynWin32_1"      ascii wide
        $a4 = "class W32API"    ascii wide
        $a5 = "FindPattern"     ascii wide
        $a6 = "HttpListener"    ascii wide
    condition:
        (2 of ($a*))                                              // cleartext in logs/memory
        or (filesize > 100KB and filesize < 2MB and #o1 > 100 and #o2 > 10 and #o3 > 10)  // obfuscated on disk
}

A Sigma equivalent for the conhost --headless launch and the PowerShell-owns-a-listener behaviour is the obvious next step if you run a SIEM.

Closing

What I enjoyed about this one was the gap between effort and payoff in the obfuscation: 326KB of string-method spaghetti that folds down to a clean little backdoor the moment you stop trying to read it and start evaluating it. The lesson I keep relearning: obfuscation that targets human eyes rarely survives a tool that treats the pure parts as constants, and Script Block Logging quietly captures the decoded result anyway.

And if you take one operational thing away: a backdoor with no C2 in it isn’t shy, it’s delegating. Go find what’s forwarding to localhost. If you’ve got Constrained Language Mode and centralised 4104 logging, most of this never gets off the ground, and you’d see it if it tried.

Stay curious, and check your scheduled tasks.


On methodology: the investigation is mine. The reverse engineering and analysis assembly were carried out with AI workflows (Claude, primarily). I reviewed every finding. Errors are mine - ping me on X or Instagram if you spot something off.

References

  • MITRE ATT&CK: T1053.005, T1564.003, T1059.001, T1140, T1562.001, T1620, T1071.001, T1055 - https://attack.mitre.org
  • ASD/ACSC Essential Eight Maturity Model (November 2023) - https://www.cyber.gov.au/business-government/asds-cyber-security-frameworks/essential-eight/essential-eight-maturity-model
  • ASD/ACSC Securing PowerShell in the Enterprise (October 2021) - https://www.cyber.gov.au/acsc/view-all-content/publications/securing-powershell-enterprise
  • ASD/ACSC Implementing Application Control (November 2023) - https://www.cyber.gov.au/business-government/protecting-devices-systems/hardening-systems-applications/system-hardening/implementing-application-control
  • ASD/ACSC Restricting Administrative Privileges (November 2023) - https://www.cyber.gov.au/business-government/protecting-devices-systems/system-administration/restricting-administrative-privileges
This post is licensed under CC BY 4.0 by the author.