Feb 7 2024 | Cody Thomas

Spinning Webs — Unveiling Arachne for Web Shell C2


Spinning Webs — Unveiling Arachne for Web Shell C2

What is a web shell?

A web shell is a payload that allows continued access to a remote system, just like other “shells” we refer to in computer security. What makes a web shell a little different is that it’s not beaconing out to a command-and-control (C2) server, nor is it bound to something locally waiting for a connection (like a port or named pipe). Instead, it’s a component of a website that’s waiting for a user to browse to it with a very specific web request. These web shell pages aren’t normally there by default, although some web services have built-in “features” to allow execution of system commands through their web interfaces.

Below is an example of a web shell in php . The code itself is straight forward — it accepts a form via a GET request with a field called cmd. That value, if present, is then executed via the system command in php and the result is returned up as part of the webpage. The example below runs the whoami command against this web shell run on a kali linux computer.

PHP Web Shell Example

A common scenario is where a website is vulnerable in some way to allow a malicious user to upload their own code that is then served up by the web server. After uploading the malicious code, the attacker can browse to their new page and execute commands on the host itself.

What does this have to do with C2?

Web shells require an operator during a red team or penetration test to repeatedly make requests in their browser or use something like curl on the command line. This works and can be helpful to test access, but this removes you from the benefits of a command-and-control (C2) platform. The operator then has to execute tasks from multiple locations and needs to keep track (and log) additional things they’re doing (often by hand). It also becomes more tedious if you’re trying to do things like uploading files. What if you could interact with a web shell as if it was any other C2 agent?

Arachne Definition in Mythic

Luckily, Mythic provides a lot of flexibility in how you design and leverage agents within the framework. The Arachne payload type leverages two of these unique flexibility options — custom tasking execution and translation containers. Let’s look at custom tasking execution first.

Custom Tasking Execution

Mythic uses a microservice architecture to allow very dynamic and modular components to interact. This sounds daunting at first, but this allows agent developers to prioritize agent development and leave the rest to somebody else. As part of that, when an operator issues a task to a callback, that tasking data goes to the agent’s Docker container for operational security checks as well as additional processing — all of which is controlled by the developer of that payload.

In Arachne’s case, a decision is made for each task — either reach out from within the Docker container to where the web shell lives directly (or via a redirector) to make the request or, if there’s another agent that’s currently linked to the Arachne callback, then prepare the message and let the other agent pick it up.

To prepare any message, Arachne uses | separated values instead of Mythic’s standard JSON format. We also want to make sure our messages are still encrypted so they don’t show up in logs or packet captures as easily. The following snippet shows a piece of the tasking creation process that prepares the message and asks Mythic to encrypt it:

async def create_go_tasking(self, taskData: MythicCommandBase.PTTaskMessageAllData) -> MythicCommandBase.PTTaskCreateTaskingMessageResponse:
  response = MythicCommandBase.PTTaskCreateTaskingMessageResponse(
  message = "{}|{}|".format(taskData.Task.AgentTaskID, base64.b64encode(self.cmd.encode('UTF8')).decode())
  encrypted_resp = await SendMythicRPCCallbackEncryptBytes(MythicRPCCallbackEncryptBytesMessage(
  if encrypted_resp.Success:
          response_data = await WebshellRPC.GetRequest(taskData.Payload.UUID,

The WebshellRPC.GetRequestfunction is part of an additional library included by Arachne for making the actual web request to the web shell so that it doesn’t have to be included with every command. After making the request, the tasking code can decrypt the response it gets back and register it for the user to see:

decrypted_resp = await SendMythicRPCCallbackDecryptBytes(MythicRPCCallbackDecryptBytesMessage(
if decrypted_resp.Success:
    await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage(
    response.Success = True

Within Mythic, this looks like the following:

LS Output from ASPX Arachne

All of this happens directly within the agent’s Docker container — no agent reached out to Mythic to fetch tasking. This just shows one example of how customized you can get in your tasking functionality. What does this flow look like? The diagram below shows all these steps in sequence:

Tasking Flow for Direct Access

However, what if the deployed web shell isn’t directly accessible by Mythic or via a redirector? What if it’s inside the target network? In that case, we can leverage the second unique component — translation containers.

Translation Containers & P2P

For another agent to pick up a message for Arachne, there must be a peer-to-peer (P2P) link between them. In the previous section, we mentioned that Arachne doesn’t use JSON, but instead uses | separated values. To satisfy these two requirements we need two things: a P2P profile and a translation container.

A P2P profile within Mythic is simple because it does nothing but identify connection parameters. There’s no additional server component waiting to accept connections from the agent because in P2P the agents talk to each other, not to Mythic. In this case, we made a P2P Profile called webshell that identifies the necessary pieces of information for a remote agent to talk to our web shell agent. This includes things like User-Agent strings, URLs, query parameters, cookies, and where the message goes in the request. This information must be part of the P2P profile and not the agent configuration so that when we tell a different agent to connect, it will be available. Below is an example of tasking a Poseidon agent to link to an Arachne agent:

Linking Dialog for Web Shells

As part of this, the Poseidonagent reports back that a new P2P connection exists over the webshell C2 profile. Now, when we issue a task to that linked callback (1198 in the above example), the Arachne code will not directly reach out, but will instead save the | separated tasking as the parameters to use.

Now in Mythic’s interface, we can see the two agents connected:

Isolated Egress Path Graph

If at any time you need to unlink the two agents, you can either issue an unlink_webshell command from poseidon, or you can right-click either agent in any graph view and manually remove the edge.

This still brings us to an issue though — the task’s parameters will be | combined, but Mythic will still try to send a JSON message of the get_tasking type. So, we need to use a translation container to fix this.

In Mythic, a translation container allows you define any communications format you desire as long as you can provide a way to translate it between your custom format and Mythic’s standard JSON format. This typically results in agents supporting their own custom binary format or other common serializable formats like Google’s protobufs and XML, but it can also be something as simple as | delineated fields.

In the code snippet below, we’re taking just the task’s parameters out from Mythic’s standard message and reporting that back as the message that should get sent to the Arachne agent.

async def translate_to_c2_format(self,
                                     inputMsg: TrMythicC2ToCustomMessageFormatMessage) -> TrMythicC2ToCustomMessageFormatMessageResponse:
  response = TrMythicC2ToCustomMessageFormatMessageResponse(Success=True)
  if "tasks" in inputMsg.Message:
      if len(inputMsg.Message["tasks"]) > 0:
          response.Message = inputMsg.Message["tasks"][0]["parameters"].encode("UTF8")
          response.Message = b""
      response.Message = b""
  return response

This function is called when Mythic has a JSON message to send to an agent, but the agent doesn’t use JSON. The translate_to_c2_format is asking for the JSON to be converted into the necessary custom special sauce. In Arachne’s case, the special sauce is just the parameters of the task, so we strip away everything else and set the new message to just be the parameters.

Likewise, when data comes back, it won’t be in Mythic’s JSON format, so a translate_from_c2_format function is called:

async def translate_from_c2_format(self,
                                       inputMsg: TrCustomMessageToMythicC2FormatMessage) -> TrCustomMessageToMythicC2FormatMessageResponse:
  response = TrCustomMessageToMythicC2FormatMessageResponse(Success=True)
  response_pieces = inputMsg.Message.decode("UTF8").split("|")
  response.Message = {
      "action": "post_response",
      "responses": [
              "task_id": response_pieces[0],
              "process_response": "|".join(response_pieces[1:]),
              "completed": True
  return response

Now the message can be processed like a normal Mythic message. However, there’s a secret third custom configuration happening here — the response from the Arachne agent is put in the process_response key rather than the user_output key. This means that instead of just shoving everything that was returned by the Arachne agent into the user’s face, we want to do one more layer of processing on it in a way that’s task specific.

Processing the Response

This is again something that’s under control of the payload developer, but the rationale here is simple — if we issue a task like pwd or ls then we want to display that data to the user; however, if we issue a task like download, then we want to intercept that output and register a file within Mythic. When using Mythic’s normal format, there are ways to do this of course, but since a web shell doesn’t allow for back-and-forth messages very easily, we’ll use this handy feature. Take the normal example here:

async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
  resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
  await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage(
  return resp

We simply take the value from the process_response key (that’s the response value) and register it as a new response for the user to see. For a download though, we can register that output as a file and make a custom message for the user to see:

async def process_response(self, task: PTTaskMessageAllData, response: any) -> PTTaskProcessResponseMessageResponse:
  resp = PTTaskProcessResponseMessageResponse(TaskID=task.Task.ID, Success=True)
  filename = pathlib.Path(task.Task.DisplayParams).name
  file_resp = await SendMythicRPCFileCreate(MythicRPCFileCreateMessage(

  if file_resp.Success:
      await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage(
          Response="Successfully downloaded filenFileID: {}".format(file_resp.AgentFileId).encode(),
      await SendMythicRPCResponseCreate(MythicRPCResponseCreateMessage(
  return resp

And in Mythic we simply see:

Raw Download Output

All these outputs can be expanded further with browser scripting to make interactive tables and buttons based on this output. For example, the download can make it much easier for the operator to download the file inline:

Download Output After Browser Script Applied

What does this entire process look like though? It’s a little more complicated than the direct access, but not by too much. The following flow diagram shows a slightly condensed version of each stage:

P2P Tasking of Linked Web Shell Agent

Wrapping up

Why did we bother going through all of this work? There’s a lot of benefits for being able to issue tasks in general through a Command and Control (C2) framework — we get per-operator tracking, timestamps, operational security checks, collaboration, dynamic responses, logging, and much more. Traditionally, there hasn’t been a workflow possible in C2 frameworks to treat a web shell like a normal beaconing agent; however, Mythic’s extreme customizability allows us to control every aspect of execution flow.

This was just one example of doing something a bit more unconventional with Mythic. By leveraging a translation container, you can use any format for agent messages, and by creating a small P2P profile, you can easily link up additional agents. Similarly, since you have full control over processing for your tasks, you can really do anything you want.

Hopefully this sparks some ideas about additional agents you can design with Mythic. Two more unconventional agents will be released for Mythic at SpecterOps’ SO-CON on March 15th.

I also wanted to give a shout out to Josiah Massari (@Airzero24) for creating the original version of Arachne a few years ago — I’m glad we could get it updated and finally over the finish line.

Spinning Webs — Unveiling Arachne for Web Shell C2 was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.