Getting a shell is the fun part
Once a proof of concept gives me command execution, the vulnerability is proven. But a Metasploit module needs a bit more love before I consider it finished.
I want it to answer things like:
- Am I talking to the right product?
- Is the target version actually vulnerable?
- Does a non-default feature need to be enabled?
- Which target and architecture should I use?
- Can I tell the difference between “not vulnerable” and “my request failed”?
- Am I dropping files or changing configuration?
- Can I clean those changes up?
That is the difference between a one-off PoC and a module I am happy to run again later.
Running a local module while developing
I keep development modules under:
~/.msf4/modules/
and mirror the normal Metasploit namespace.
For example:
~/.msf4/modules/exploits/linux/http/
~/.msf4/modules/auxiliary/admin/http/
A typical test cycle is:
cp my_module.rb ~/.msf4/modules/exploits/linux/http/
msfconsole
and inside Metasploit:
reload_all
search my_module
use exploit/linux/http/my_module
info
options
advanced
I use reload_all a lot while writing modules. It is faster than restarting msfconsole for every small change.
Build check early
I like to get check working before the exploit is polished.
The flow is usually:
identify the product
↓
identify the version or feature
↓
do a safe vulnerable-behavior check if possible
↓
return a useful CheckCode
A web server banner alone is rarely enough. I prefer something product-specific: a version endpoint, a known page, a response marker, or a harmless request that confirms the vulnerable code path.
This also makes development nicer because I can repeatedly test the target without firing the payload every time.
Be explicit about the things the exploit needs
Some modules only work when one extra condition is true.
Apache Spark CVE-2022-33891 is a good example: the vulnerable path depends on ACL support being enabled. That kind of condition belongs in the module logic and the documentation, not hidden in a comment somewhere.
The same goes for:
- authentication;
- writable directories;
- optional services;
- a specific CPU architecture;
- a second vulnerability;
- a credential or secret recovered earlier in a chain.
For a chain I tend to think in steps:
get information
↓
recover secret / credential
↓
create authenticated state
↓
reach command execution
↓
deliver payload
If step two fails, I want the module to say step two failed.
Keep payload delivery as simple as possible
Once command execution works, I decide how to get a useful session.
Common options are:
- direct command payload;
- encoded command;
- command stager;
- write a file and execute it;
- use an interpreter already present;
- drop a native payload.
I normally start with the least complicated option and only move to a command stager or native dropper when the target needs it.
Complex payload delivery is harder to debug and gives you more things to clean up.
Cleanup is part of the module
If I create a file, plugin, cron job, user or temporary directory, I try to register it for cleanup.
And when cleanup is impossible, I document the side effect.
A good module should leave the target in roughly the same state it found it, apart from the session you deliberately opened.
Use the framework instead of reinventing it
Metasploit already has a lot of useful helpers. I try to use the HTTP, authentication and command-stager mixins instead of writing my own plumbing.
It gives you:
- consistent proxy handling;
- SSL support;
- payload staging;
- common error handling;
- easier review by the Metasploit team.
It also makes the module easier for somebody else to read.
Keep the CVE story with the CVE
I have written quite a few modules over the years, and a big list of module descriptions quickly turns into a second vulnerability archive.
That is not what I want here.
The vulnerability-specific analysis, official module and contribution PR live on the related Research page. This page is for the bits I reuse while building a module.
For example:
Official module versus my development copy
Once a module lands in the official Metasploit Framework, that is the version I want people to use.
My rule for the site is simple:
official module exists
→ link the official module
→ link the contribution PR when I know it
→ no duplicate local .rb download
no official module exists
→ link my research implementation
→ add the contribution PR when applicable
The PR is still useful because it shows the discussion and review history. The official module is the maintained code.
My quick pre-PR checklist
Before submitting a module I normally check:
rubocop/ formatting is clean;- references and authors are correct;
checkworks;- required options are really required;
- targets and architectures are accurate;
- failure messages are useful;
- temporary files are cleaned;
- no lab IPs, passwords or debug prints are left behind;
- the description explains the vulnerability without requiring the advisory open next to it.
Then I test it again from a clean Metasploit checkout. That catches a surprising number of “works on my box” mistakes.