Developing Metasploit Python Modules the Easy Way
Introduction
For a while now, the Metasploit Framework has supported modules written in languages other than Ruby (e.g. Python, Golang) which is great if you’re not a Ruby inclined person such as yours truly.
We aren’t going to dive into how Metasploit’s “External Modules” work (you can find all the nitty-gritty details in the “External Module” section of the official docs here), rather, this blog is meant to be a quick-start guide on how to easily develop external Metasploit modules in Python. While the official docs do cover things pretty well I did run into a bunch of “gotchas” which I’ll be talking about in this blog post.
Along with this blog, I’m releasing on Github the msf-modules-python template repository that huntr researchers and the general infosec community can use to easily get started developing Python Metasploit modules.
The Basics
A Python Metasploit module consists of a single `.py` file. In order for Metasploit to recognize and run it, it needs to be in a specific location on disk and marked as executable. Additionally you also need to configure your PYTHONPATH environment variable to point to the ‘metasploit’ Python library which is distributed with Metasploit and is a bit hidden in its installation folder. (All of these steps are all automated if you use the msf-modules-python template 🔥).
One nice thing about external MSF modules is you can also run them “standlone-ish” (e.g. without msfconsole). I say “standalone-ish” because, for Python modules at least, while you can run them without msfconsole you still need the metasploit Python library which isn’t on PyPi and seems to be only distributed along with Metasploit.
Setting up a development environment manually (a.k.a. the hard way)
(Again, all of these steps are all automated if you use the msf-modules-python template 🔥 🎉)
If you want to go through the process of setting up a development environment for Python modules manually, you would do the following:
- Install Metasploit (which can be an adventure in itself)
- Create a custom module directory structure with the following command ‘mkdir -p $HOME/.msf4/modules/exploits/custom’
- Grab an example Python Module from here and save it in the folder created in step 2.
- Mark the python file as executable: ‘chmod +x $HOME/.msf4/modules/exploits/custom/my_module.py’
Finally, you have to modify your PYTHONPATH environment variable for the module to pick up the `metasploit` Python library. This is required for logging and to allow the Python module to communicate with Metasploit. Hence if you don’t do this, Metasploit won’t be able to run the Python module.
Unfortunately, the Metasploit install folder location varies between Linux distros (e.g. Kali Linux). So the easiest way I’ve found to locate the Metasploit Python library is to run the following command:
You then need to export the path up until the ‘/python’ folder. On Debian this would be “/opt/metasploit-framework/embedded/framework/lib/msf/core/modules/external/python". The full command would be the following (you’d also probably want to add this to your .bashrc file).
Using the msf-modules-python template (a.k.a the EZ way)
The msf-modules-python template takes advantage of VSCode devcontainers to create a portable development environment for creating Python Metasploit Modules. It effectively allows you to code inside a Docker container with VScode, as such the only prerequisites needed to use the msf-modules-python template are installing VSCode and Docker Desktop.
The end result will be a complete development environment with Metasploit installed and all the configuration/environment variables mentioned above already setup so you can concentrate on just coding.
To get started, browse to https://github.com/protectai/msf-module-python and click the green “Use this template” button:
This will prompt you to create a new repository using this one as the template meaning it will retain the files and folder structure. Give your repo a name and hit ’ “Create Repository button”:
Now pull down the repository locally, I named my repo ‘my-1337-rce` in the above example so change that to whatever you named it:
Open VSCode and first install the “Remote Development” Extension, this will allow you to code inside the Docker container which is defined in the ‘.devcontainer` folder in the repository.
Now open the repository you’ve cloned and VScode should prompt you to Re-Open the repository in the devcontainer. Click the “Reopen in Container” button.
VScode will start building the docker image and configuring the container. You can take a look at the progress by clicking the “Show logs” button in the bottom right corner.
After everything’s done, you can open the VSCode terminal and you’re now ready to code inside the VSCode devcontainer:
Metasploit is installed and ready to go with all the necessary configuration, let’s spin up msfconsole (on first run it’ll ask you to setup the database):
Let’s try loading one of the example Python modules included in the template repository which will be available under the ‘exploit/custom’ path:
You can see MSF picks up the modules under the ‘msfmodules’ folder in the repository. As the PYTHONPATH environment variable is already setup you can also run the modules “standalone” (e.g. not through msfconsole) out of the box:
Writing a basic RCE Metasploit Python Module
Now that you have a development environment setup (hopefully you used the EZ way with the msf-modules-python repository 🤘), let’s write a basic RCE module.
For this tutorial, we’re going to develop a module to exploit the Ray cpu_profile command injection vulnerability (CVE-2023-6019) found by huntr researcher Sierra Haex. You can find the full code for this module on Github here and the original Huntr report for the bug here.
The vulnerability is extremely simple. Older versions of Ray have a command injection vulnerability in the cpu_profile API endpoint. User input isn’t sanitized in the format URL parameter leading to command execution. This can be easily exploited using the following cURL one liner:
Let’s make a Python Metasploit module to exploit this vulnerability and pop a Meterpreter shell 🐚
First thing we need to do is set up a vulnerable Ray instance.
If you’re on Mac with an M-Series chip run the following command:
For everyone else you can run the following:
After docker pulls down the image and starts the container you’ll have the Ray dashboard available at localhost:8265.
Now let’s start writing our MSF module, we’ll use the ‘my_rce_module.py’ file from msf-modules-python repository as a starting point.
First thing we need to do is add in some basic information in the metadata variable about the exploit, this is what shows up in msfconsole when you run the “show options” or “show info” commands. Change the name, description, author and references:
‘my_rce_module.py’ is setup as a RCE module already, hence the ‘type’ being remote_exploit_cmd_stager (to see a full list of module types take a look at the section in the official docs).
The only thing we really need to change here is the ‘rport’ option. Since Ray’s dashboard is by default on port 8265 let’s change that:
Before we start writing the logic for the exploit, let’s try exploiting this using cURL to understand what the server response looks like when the exploit is successful:
The server returns a “500 Internal Server Error” with the response body containing some very unique output so we can “key” off of that to tell the user if the exploit was successful or not.
Now all we have to do is write some Python code to issue this HTTP request in the `run` function of our Python module:
Line 85-94 was already written for us and this basically just sets up the logging and constructs the base URL based on the module options. All we had to write was line 96-112.
On line 97 we’re simply making a HTTP request using the Python requests library to the vulnerable URL. In order for Ray to process the request, we need to specify a ‘pid’ URL parameter which can be any number. Here we’re using `random.randint()` to give that parameter a random number.
The actual command needed to pop a Meterpreter shell is given to the Python module by Metasploit at runtime as the ‘command’ argument. So all we do on line 101, is give the vulnerable ‘format’ URL parameter the command that’s passed to the module by Metasploit.
Finally on line 111-112, we check if the server response body and the status code match up to what we’d expect if the exploit worked and tell the user the exploit succeeded.
Let’s try running our module!
It looks like it worked, but it doesn’t drop us into the Meterpreter prompt. The problem here is that the Ray server process blocks on the command Metasploit gives us to stage and execute Meterpreter on the remote server.
So how do we fix this? Thankfully there's a “MeterpreterTryToFork” payload option! As the name implies this will instruct Meterpreter to attempt to “background” itself on execution which will in turn not block the Ray server process. Let’s try setting that payload option and re-running the module:
Success! We got a shell 🐚! Now all we need to do is set the “MeterpreterTryToFork” option as default in the metadata section so that the user doesn’t have to know this is required, we can do that simply by adding the following to the metadata variable dictionary:
Congrats! You’ve successfully written your Python Metasploit RCE module!
Python Module Development Gotchas, Tips & Tricks
“Module exited Abnormally” Error
If you get a “Module exited abnormally” error when running your module in msfconsole this could mean either there’s a bug in your Python Module or your module isn’t marked as executable. Make sure all your Python modules are executable (e.g. chmod +x).
The “Everything is a String” Gotcha
At the time of writing, all options you “set” in msfconsole are passed as strings to the Python Module during its execution regardless whether it’s a port number (integer), a switch (boolean) etc…
Since I wanted the options I defined in the modules to be in their correct types, I created a small utility function that “casts” the arguments passed to the module to their correct types. (The function is present in the example modules in the msf-modules-python repository).
The args variable will have the arguments “casted” to their correct types.
Debugging Python Modules
If you’re module has a bug, Metasploit won’t directly tell you what went wrong:
To find out what’s “abnormal” about your module, you’re going to have to take a peek in the Metasploit logs which are located in the file at “$HOME/.msf4/logs/framework.log”. You’ll find the Python Tracebacks necessary to debug your modules there:
Setting Default Module Options
It wasn’t possible to set default options for external modules until recently after I bugged the Metasploit devs on Github. As of writing you can now set default module options by using the ‘default_options’ key in the metadata dictionary. E.g. if you wanted to set the ‘MeterpreterTryToFork’ payload option to default to True you would specify the following in the metadata variable:
Third-party Python libraries in your modules
Technically you can use any third party libraries in your Python Metasploit modules but it would require the user to install them. My recommendation is stick with as few third party libraries as possible.
Metasploit doesn’t “see” my module
This can be because your module isn’t marked as executable (e.g. chmod +x) or you’ve named your module file in a way that Metasploit doesn’t like. Make sure your module file name is all lowercase and follows the snake case convention (e.g. lowercase words separated by underscores: my_leet_rce.py).
Conclusion
As you can see, writing Python Metasploit modules is a powerful way to exploit vulnerabilities, and with tools like the msf-modules-python template, it's easier than ever to get started. If you're interested in taking your skills to the next level and exploring AI/ML bug bounty hunting, be sure to check out our beginner's guide over at huntr.com. It’s the perfect starting point for anyone looking to dive into this field. Happy hunting!