How to Start Contributing to Metasploit: Field Notes from 68 Modules
Table of Contents
TL;DR
68 modules, 70 merged PRs, 2.5 years. This post covers what the official docs don’t: how to pick targets, what reviewers actually flag, the Ruby patterns that matter, how to clean up properly, and every mistake I made so you don’t have to. Not a beginner tutorial - read the official contributing guide first. This is the field manual on top.
What’s in here:
- How to get your first PR merged (including without writing an exploit)
- The check method rules that trip everyone up
- Cleanup patterns that protect real production systems
- What the review process actually looks like (with links to all 70 of my PRs)
- Ruby tips specific to Metasploit, not vanilla Ruby
- Why finding your own vulns is the best strategy
All 68 modules with source and documentation: msf-exploit-collection.
This is the companion post to From Zero to Exploit Dev, which covers the full journey - how I started, why I kept going, what it cost. Read that one first if you want the context. This post is what comes after: the practical playbook for actually getting your code into Metasploit.
This article is for people I know - friends who’ve told me they wanted to contribute but never did. Some were intimidated by the review process, some didn’t know where to start, some just never pulled the trigger. If that’s you, read this. There’s nothing to be afraid of.
The easiest first contribution
You don’t even need to write an exploit from scratch. There are existing modules in Metasploit that have no documentation. Check the issues for modules missing their .md doc file. All you have to do is:
- Spin up the lab (Docker, VM, whatever)
- Run the module, make sure it works
- Write the documentation (setup instructions, expected output, screenshots)
- Submit the PR
That’s it. No Ruby, no exploit dev, just testing and writing. It’s a quick win that gets your name in the repo, gets you familiar with the review process, and costs nothing. And if while testing you notice something that could be improved in the module itself - a missing edge case, a better check method, a cleanup that’s not there - fix it and include it in the same PR. The reviewers love that.
Ship fast
Forget ExploitDB. Follow researchers on Twitter/X, watch for fresh CVE drops, grab the PoC from the researcher’s GitHub. If you wait a week, someone else already submitted the PR. Check the issue tracker for suggestion-module and assign yourself before you start.
I have friends who spent time writing a module, went to submit, and found out someone already had a PR open for the same CVE. It happens. Don’t be discouraged - if someone beat you to it, go read their PR and the review comments. Compare their code with yours. See what they did differently, what the reviewers asked them to change, and what you would have gotten wrong. That’s free education. And next time, you’ll be faster.
Better yet: find your own vulns. Nobody can beat you to a PR if the CVE is yours. That’s how I ended up with 73 CVEs and 68 modules.
The check method
Exploit::CheckCode::Vulnerable = you triggered the bug and got proof. Exploit::CheckCode::Appears = you matched a version or behavior. Exploit::CheckCode::Detected = the target runs the software but you can’t confirm it’s vulnerable. Don’t mix them up. Reviewers will push you toward the most accurate one.
Never use fail_with inside check. The check method must always return a CheckCode. fail_with is for the exploit method only. This also applies to shared helpers - if a function is called from both check and exploit, make sure it doesn’t call fail_with when called from check.
# bad
def check
res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'api'))
fail_with(Failure::Unreachable, 'No response') unless res
end
# good
def check
res = send_request_cgi('uri' => normalize_uri(target_uri.path, 'api'))
return CheckCode::Unknown('No response from target.') unless res
return CheckCode::Safe('Not the expected application.') unless res.body.include?('MyApp')
version = res.body.match(/version.*?(\d+\.\d+\.\d+)/)&.captures&.first
return CheckCode::Detected('Could not extract version.') unless version
return CheckCode::Appears("Version #{version} in vulnerable range.") if Rex::Version.new(version) < Rex::Version.new('3.2.1')
CheckCode::Safe("Version #{version} is not vulnerable.")
end
Use the payload framework
No hardcoded bash -i >& /dev/tcp/.... Use execute_command(), CmdStager, the payload mixins. Your module should work with cmd, meterpreter, and whatever else the pentester picks.
If your payload goes through a shell, bad characters like ' will break it. Use Rex::Text.encode_base64 and pipe through base64 -d instead of passing raw payload strings.
Exception: sometimes raw bash is the only way. Comment why and the reviewer will understand.
Cover as many targets as possible
Don’t blindly trust CVE advisories for affected version ranges. They’re often wrong, incomplete, or copy-pasted from the vendor who didn’t check properly. Diff the code yourself between releases, find the commit that introduced the vuln, find the real lower bound. Between versions, parameters get renamed, routes change, checks get added. Code review is the only reliable source of truth.
If it’s not Linux-only, make it multi/ and handle both Unix and Windows.
Clean up after yourself
Pentesters aren’t supposed to use Metasploit on internal engagements. But they do - it just doesn’t end up in the report. They’ll run your module, get the shell, move on. Most won’t check what your exploit left behind on the target.
Clean up within the exploit flow itself. Don’t rely on on_new_session (meterpreter only). If you can send a cleanup request right after the payload executes, do that.
If cleanup could break the target, document it instead of auto-cleaning.
def exploit
create_admin
poison_cache
execute
ensure
cleanup_admin
cleanup_cache
end
ensure is attached to the def - no begin needed. The framework handles exceptions, you don’t begin/rescue yourself. ensure runs no matter what, even if the exploit fails mid-chain. That’s your safety net for cleanup.
Your code runs in the real world
This isn’t a CTF. Your module will run on real networks, against real servers, during real pentesting engagements. A buggy check method means a false positive in a report that lands on a CISO’s desk. A missing cleanup means artifacts left on a production server. An exploit that crashes the service means downtime for a client.
The pentester using your module trusts that you did the work properly. That’s the responsibility nobody mentions.
Be smart about state and performance
Reuse state between check and exploit. If your check discovers a token, version, or endpoint, store it in an instance variable and reuse it in exploit. Don’t fetch the same thing twice.
@token = @token || find_token
sleep() is a code smell. If you’re sleeping to wait for something, use retry_until_truthy instead. It polls with a condition and a timeout. Reviewers know the difference.
Randomize everything you can. Hardcoded filenames, paths, parameter names - if it can be random, make it random. Rex::Text.rand_text_alpha(8) is your friend.
Move logic into helper methods. If your exploit method has a helper that returns true/false and then you branch on it - just put the fail_with inside the helper. Cleaner, less branching.
Destructive operations
If your exploit drops tables, writes files, or modifies configs, the reviewer will question the Rank. If you use DROP TABLE, it better be on a temporary table you created, not the target’s data. Explain your cleanup strategy.
If your module deletes files on the target (even temporarily), reviewers might ask for a DefangedMode option that lets the user run it safely. Document side effects honestly in the Notes hash - ARTIFACTS_ON_DISK, CRASH_SERVICE_DOWN, IOC_IN_LOGS.
Documentation and references
Documentation must match the code. If your module uses RHOST (singular) but your doc says set RHOSTS, it’s wrong. Copy-paste your actual console output, don’t fake it.
Don’t put CVE numbers in the module Name. The Name field is for what the module does. References are for CVEs.
Commit hashes in references. When you reference a vulnerable commit, make sure it’s from the right repo. I once pointed to a commit in a sub-repo instead of the main one. The reviewer caught it.
Comment the tricky parts. If your exploit chain has a non-obvious step, explain it. A one-line comment saves a round of review.
Debugging: set HttpTrace true
This is the single most useful debugging trick nobody tells you about. Run set HttpTrace true in the msfconsole before executing your module. Every HTTP request and response gets printed in the console - full headers, full body, everything.
No Burp, no mitmproxy, no external tool. You see exactly what your module sends and what the server returns. When your exploit fails silently, this is how you find out why in 5 seconds instead of 30 minutes.
Combine it with set VERBOSE true to also get vprint_status output. Between the two, you see everything your module does under the hood.
And stop hitting the up arrow 47 times to find that set RHOSTS command you typed 20 minutes ago. Use resource files:
# save this as dev.rc
use exploit/multi/http/my_module
set RHOSTS 192.168.1.100
set RPORT 8080
set LHOST 192.168.1.50
set LPORT 4444
set HttpTrace true
set VERBOSE true
run
msfconsole -r dev.rc
One file, one command, same setup every time. When you’re iterating on a module and restarting msfconsole 30 times a day, this saves your sanity.
If you see shared logic, write a mixin
If you’re writing multiple modules for the same product and you see the same code copy-pasted between them - version detection, auth flow, response parsing - don’t duplicate it. Write a mixin and share it.
I wrote 3 modules for MajorDoMo, 5 for SPIP, 3 for AVideo. Each group shares check methods, auth flows, and target detection via shared mixins. If two modules share 50 lines of logic, that’s a mixin waiting to be extracted.
Ruby, the short version
- Use builtins.
Base64.encode64,SecureRandom.hex,URI.encode_www_form. Don’t rewrite what exists. - Ternaries are fine when simple. Don’t nest them.
- Skip
else. Useunless, early returns, guard clauses. One indentation level max. ||=for conditional assignment,&.for safe navigation,unlessinstead ofif !.- Don’t
begin/rescue. The framework handles exceptions. Usefail_withfor expected failures inexploit, let everything else bubble up.
Things people forget
prepend Msf::Exploit::Remote::AutoCheck- runs check automatically before exploit. Reviewers expect it.vprint_statusfor debug,print_statusfor the user. Don’t spam the console.- Test with at least 2-3 different payloads. Not just the default one.
- The
Noteshash is mandatory.Stability,Reliability,SideEffects. - Read recently merged PRs for current style expectations.
- One module per PR. Always.
- Run
msftidyas a pre-commit hook before submitting. - Don’t default optional options. If an option is not required, don’t give it a default value.
You don’t need to be a Ruby expert
There are active contributors who don’t master Ruby. They still ship modules. Metasploit Ruby is not vanilla Ruby - it’s mostly mixins, DSL-style calls, and framework conventions. You learn the patterns by reading existing modules, not by studying Ruby.
If you can read a Python PoC and understand the logic, you can write a Metasploit module. The Ruby part is just syntax.
The review process
They almost never close PRs. They work with you and tell you exactly what to fix. Expect 2-3 rounds of feedback on your first module. Don’t take it personally, don’t give up. Ask questions in the PR if you’re stuck - they’re there to help, not to gatekeep.
The only way to get your PR closed is to abandon it or submit something that clearly doesn’t work.
AI is fine
The barrier has never been lower. Use agents to scaffold, write boilerplate, translate PoCs. But test everything yourself and understand every line. The reviewers have seen AI-generated code that looked perfect and did nothing. Use AI to go fast. Use your brain to go right.
My PRs (read the review comments)
The best way to learn is to read real reviews. Here are all my merged PRs - click any of them and read the conversation between me and the reviewers. You’ll learn more from that than from any guide.
- #21078 - Fix ChurchCRM unauthenticated RCE module
- #21076 - Add AVideo Encoder getImage.php command injection (CVE-2026-29058)
- #21075 - Add AVideo catName blind SQLi credential dump (CVE-2026-28501)
- #21069 - Add FreeScout ZWSP .htaccess unauthenticated RCE (CVE-2026-28289)
- #21034 - Add openDCIM install.php SQLi to RCE
- #21017 - Add Tactical RMM Jinja2 SSTI RCE (CVE-2025-69516)
- #21006 - Add Ollama path traversal RCE (CVE-2024-37032)
- #21003 - Unified Selenium Grid/Selenoid RCE
- #21002 - Add LeakIX search module
- #21001 - Add SPIP Saisies plugin RCE (CVE-2025-71243)
- #21000 - Add three MajorDoMo unauthenticated RCE modules
- #20938 - Fix BeyondTrust PRA/RS exploit
- #20793 - Add AVideo notify.ffmpeg.json.php unauth RCE (CVE-2025-34433)
- #20791 - Add Web-Check screenshot API RCE (CVE-2025-32778)
- #20785 - Update react2shell: Add Waku framework support
- #20768 - Add Gladinet CentreStack/Triofox modules
- #20767 - Add GeoServer WMS GetMap XXE (CVE-2025-58360)
- #20761 - Add WordPress ACF Extended RCE (CVE-2025-13486)
- #20746 - Add WordPress King Addons privesc (CVE-2025-8489)
- #20740 - Separate SSL and SRVSSL options
- #20725 - Add Magento SessionReaper (CVE-2025-54236)
- #20720 - Add WordPress AI Engine MCP RCE (CVE-2025-11749)
- #20719 - Add FreePBX filestore RCE (CVE-2025-64328)
- #20718 - Add Monsta FTP downloadFile RCE (CVE-2025-34299)
- #20713 - Add N-able N-Central auth bypass and XXE (CVE-2025-9316)
- #20710 - Add GHSA and OSV reference type support
- #20705 - Add Flowise RCE exploits (CVE-2025-59528, CVE-2025-8943)
- #20633 - Fix typo in FreePBX module
- #20540 - Add helpful tips to COMMON_TIPS
- #20455 - Add Aitemi M300 unauth RCE (CVE-2025-34152)
- #20446 - Add ICTBroadcast unauth RCE (CVE-2025-2611)
- #20421 - Fix get_nonce in WP Ultimate Member SQLi
- #20382 - Fix WP Depicter SQLi
- #20375 - WP Photo Gallery SQLi (CVE-2022-0169)
- #20365 - Fix Maltrail RCE
- #20364 - Add WingFTP unauth RCE (CVE-2025-47812)
- #20338 - Add Xorcom CompletePBX modules (CVE-2025-2292)
- #20293 - Add Mr. Robot Easter egg
- #20235 - vBulletin replaceAdTemplate RCE
- #20214 - Add Invision Community customCss RCE (CVE-2025-47916)
- #20187 - Add CVE-2025-27007 to WP SureTriggers
- #20185 - Add WP Depicter SQLi (CVE-2025-2011)
- #20159 - Add WP User Registration privesc (CVE-2025-2563)
- #20146 - Add WP SureTriggers admin-creation & RCE (CVE-2025-3102)
- #20085 - Add Craft CMS Preauth RCE (CVE-2025-32432)
- #19713 - Add WP Time Capsule RCE (CVE-2024-8856)
- #19661 - WP Really Simple Security auth bypass to RCE (CVE-2024-10924)
- #19608 - Add CyberPanel Pre-Auth RCE (CVE-2024-51378)
- #19527 - Add bypass for GiveWP RCE (CVE-2024-8353)
- #19517 - Add TI WooCommerce Wishlist SQLi (CVE-2024-43917)
- #19497 - Add Msf::Exploit::Remote::HTTP::Wordpress::SQLi mixin
- #19489 - Add WP wp-automatic SQLi to RCE (CVE-2024-27956)
- #19488 - Add WP Ultimate Member SQLi (CVE-2024-1071)
- #19485 - Add BYOB unauth RCE (CVE-2024-45256)
- #19482 - Add LearnPress SQLi (CVE-2024-8522)
- #19473 - Add WP Fastest Cache SQLi (CVE-2023-6063)
- #19456 - Add VICIdial authenticated RCE (CVE-2024-8504)
- #19453 - Add VICIdial SQLi (CVE-2024-8503)
- #19444 - Add SPIP BigUp unauth RCE (CVE-2024-8517)
- #19432 - Refactor SPIP modules + SPIP mixin
- #19424 - Add GiveWP RCE (CVE-2024-5932)
- #19417 - Simplify WP Backup Migration exploit (CVE-2023-6553)
- #19394 - Add SPIP unauth RCE
- #19208 - Add WP Hash Form RCE (CVE-2024-5084)
- #19071 - Add AVideo WWBNIndex RCE (CVE-2024-31819)
- #18891 - Add WP Bricks Builder RCE (CVE-2024-25600)
- #18630 - Add MajorDoMo command injection (CVE-2023-50917)
- #18577 - Add Splunk Enterprise RCE (CVE-2023-46214)
- #18567 - Add WP Royal Elementor Addons file upload (CVE-2023-5360)
- #18542 - Add Vinchin Backup command injection
Official docs
- Contributing to Metasploit
- Creating Your First PR
- Get Started Writing an Exploit
- Common Module Coding Mistakes
- Guidelines for Accepting Modules
- Handling Failures with fail_with
- How to cleanup after execution
- Writing Module Documentation
Metasploit is not a CTF tool. Some people treat it that way, some people call it a skid launcher. But the real game is in the code. Writing modules teaches you how vulnerabilities actually work, how exploit chains are built, how payloads interact with targets. Reading and writing Metasploit code is one of the best exploit dev educations that exists, and it’s free.
You’re not working for free
Every module you land gets mentioned in the Rapid7 weekly wrap-up blog posts. Your name is in the release notes. Your module is in the framework that millions of people use. That’s visibility you can’t buy.
It’s on your GitHub, it’s on your resume, and when recruiters or hiring managers look you up, they see merged PRs in one of the most recognized security tools in the world. That’s how doors open.
Here’s what it looks like: the 2025 Annual Wrap-Up lists top contributors for the year. The weekly wrap-ups mention your module, your name, and link to your PR. I’ve been featured in 30 of them. That’s 30 times Rapid7 published my name on their blog. For free.
And honestly, most people won’t understand what you did. Your family won’t get it, your non-tech friends won’t get it. That’s fine. You don’t need them to. Having your name in Metasploit is already cool in itself. The people who matter in this industry will know exactly what it means.
Note: Some parts of this article may become outdated. The Metasploit team has discussed automating lab environments via Docker as part of their GSoC ideas, which would make setting up test targets much easier. The process is evolving - check the wiki for the latest.
The one thing I did that mattered more than anything technical: I never asked for permission. I didn’t wait for someone to invite me. I didn’t look for a mentor. I didn’t take a course. I read a module, copied the structure, broke everything, fixed it, and submitted. When it got torn apart in review, I fixed it again. 70 times.
Don’t do this for your resume. Do it because it’s fun. The day it stops being fun, stop. I wrote 68 modules because I couldn’t stop. That’s it.
If you want the full story - how I went from knowing nothing in 2020 to 73 CVEs and 68 Metasploit modules - read From Zero to Exploit Dev. That post is the journey: the failures, the grind, what it cost, what it gave. This post is the playbook: the practical stuff, the patterns, the review traps, the things I learned by doing it 70 times. Start there for the why, come here for the how.
To the friends who grind at 2am and never put their name on anything: the code you write in silence is worth nothing if nobody sees it. Push it. Get visible. Submit the PR.