DEV Community

lupingQAQ
lupingQAQ

Posted on

dotnet-memshell: three .NET memory shells at three insertion positions

dotnet-memshell: three .NET memory shells at three distinct insertion positions

Three .NET memory shells at three different insertion positions - single-file payloads, armed by one deserialization, invisible to unmarked traffic.

Why these are different

Most public .NET memory shells (module, handler, route, VirtualPathProvider) live inside the HTTP request pipeline at already-known positions. dotnet-memshell spreads across three different layers instead: the pipeline event layer (with pool broadcast), the HTTP-to-WebSocket upgrade point (connection takeover - the .NET port of the WebSocket memory-shell idea), and an already-registered Remoting channel (container-style endpoint registration).

All three ride the target's existing endpoints: no new port, no web.config change, no file on disk.

The three shells

# Shell Insertion position Trigger Minimum privilege
1 HttpModuleShell HttpApplication module-event pipeline any request with an MSH-Cmd: <cmd> header default app-pool identity
2 WsTakeoverShell HTTP-to-WebSocket upgrade transition handshake with subprotocol msh app-pool identity + integrated pipeline + WS feature
3 RemotingUriShell AppDomain-global URI table of an existing Remoting channel remoting call to <channel>/msh app-pool identity + business hosts Remoting
HTTP.sys / IIS
 +- ASP.NET request pipeline
     +- module events (BeginRequest...)      <- #1 HttpModuleShell
     +- routing / VPP / handler / endpoint    (known families)
     +- upgrade point (HTTP->WS handshake)   <- #2 WsTakeoverShell

in-process protocol stacks
 +- existing Remoting server channel          <- #3 RemotingUriShell
Enter fullscreen mode Exit fullscreen mode

What makes them different

Property dotnet-memshell
New listening port none - #1/#2 ride the site's 80/443, #3 rides the business channel
web.config / registry / URLACL changes none (contrast: HttpListener shells need netsh urlacl, an admin action)
Files dropped none - payloads are single .cs files compiled into the deserialization, assembly loaded from bytes (Location empty)
Effect on unmarked traffic none - every trigger checks a magic marker first and returns immediately
Pool coverage HttpApplicationFactory._freeList broadcast - arming only the serving instance misses most requests
Graceful degradation #2 on non-integrated pools catches and passes through (the handshake request still returns 200)

Call chains

#1 HttpModuleShell

delivery deserialization -> E()
 +- HttpContext.Current.ApplicationInstance                 (serving instance)
 +- HttpApplicationFactory._theApplicationFactory._freeList  (pool broadcast)
      per instance:
      +- integrated: GetModuleContainer(<module>)
      |    -> new SyncEventExecutionStep(app, OnRequest)     [reflection]
      |    -> ModuleContainer.AddEvent(BeginRequest, false, step)
      +- classic:    ApplicationStepManager._execSteps append  [append-only]
trigger: request -> BeginRequest -> step -> OnRequest
      +- no MSH-Cmd header -> return (business continues untouched)
      +- header -> cmd.exe /c -> Response.Write -> End
Enter fullscreen mode Exit fullscreen mode

#2 WsTakeoverShell

same anchor; handler is a strict no-op unless handshake + msh
GET + Upgrade: websocket + Sec-WebSocket-Protocol: msh
 -> IsWebSocketRequest check -> AcceptWebSocketRequest(Duplex)
 -> connection upgraded, request pipeline terminates, duplex stream owned by the loop
 -> Receive(cmd) -> exec -> Send(output) ... until Close frame
Enter fullscreen mode Exit fullscreen mode

#3 RemotingUriShell

E()
 +- AppDomain.AssemblyResolve  <- bridges byte-loaded assembly (formatter resolves
 |                                well-known types by assembly NAME)
 +- guard: an IChannelReceiver already exists (business hosts remoting)
 +- RegisterWellKnownServiceType(E.Svc, "msh", Singleton)
trigger: remoting client -> tcp://host:<business-port>/msh -> Svc.Exec(cmd)
Enter fullscreen mode Exit fullscreen mode

Notes

  • Payloads are delivered as single .cs source files, compiled into the deserialization payload; the assembly is loaded from bytes, so Assembly.Location is empty.
  • Each trigger checks a magic marker before doing anything, so unmarked business traffic is untouched.
  • For .NET Framework 4.5+.

Repo: https://github.com/lupingQAQ/dotnet-memshell

Top comments (0)