CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/tools/payloads/ysoserial/find_ysoserial_offsets.rb
Views: 11623
1
#!/usr/bin/env ruby
2
3
require 'diff-lcs'
4
require 'json'
5
require 'base64'
6
require 'open3'
7
require 'optparse'
8
9
YSOSERIAL_RANDOMIZED_HEADER = 'ysoserial/Pwner'.freeze
10
PAYLOAD_TEST_MIN_LENGTH = 0x0101
11
PAYLOAD_TEST_MAX_LENGTH = 0x0102
12
YSOSERIAL_MODIFIED_TYPES = %w[bash cmd powershell].freeze
13
YSOSERIAL_UNMODIFIED_TYPE = 'none'.freeze
14
YSOSERIAL_ALL_TYPES = ([YSOSERIAL_UNMODIFIED_TYPE] + YSOSERIAL_MODIFIED_TYPES).freeze
15
16
@debug = false
17
@generate_all = false
18
@payload_type = YSOSERIAL_UNMODIFIED_TYPE
19
@ysoserial_payloads = []
20
@json_document = {}
21
OptionParser.new do |opts|
22
opts.banner = "Usage #{File.basename($PROGRAM_NAME)} [options]"
23
24
opts.on('-a', '--all', 'Generate all types of payloads') do
25
@generate_all = true
26
end
27
28
opts.on('-d', '--debug', 'Debug mode (output offset information only)') do
29
@debug = true
30
end
31
32
opts.on('-h', '--help', 'Help') do
33
puts opts
34
abort
35
end
36
37
opts.on('-m', '--modified [TYPE]', String, 'Use \'ysoserial-modified\' with the specified payload type') do |modified_type|
38
@payload_type = modified_type
39
end
40
41
opts.on('-p', '--payload [PAYLOAD]', String, 'Specified ysoserial payload') do |payload|
42
@ysoserial_payloads << payload
43
end
44
45
opts.on('-j', '--json [PATH]', String, 'Update an existing JSON document') do |json_path|
46
@json_document = JSON.parse(File.read(json_path))
47
end
48
end.parse!
49
50
def generate_payload(payload_name, search_string_length)
51
# Generate a string of specified length and embed it into an ASCII-encoded ysoserial payload
52
search_string = 'A' * search_string_length
53
54
# Build the command line with ysoserial parameters
55
if @payload_type == YSOSERIAL_UNMODIFIED_TYPE
56
stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-original.jar', payload_name, search_string)
57
else
58
stdout, stderr, _status = Open3.capture3('java', '-jar', 'ysoserial-modified.jar', payload_name, @payload_type, search_string)
59
end
60
61
payload = stdout
62
payload.force_encoding('binary')
63
64
if @debug && payload.empty? && !stderr.empty?
65
# Pipe errors out to the console
66
warn(stderr.split("\n").each { |i| i.prepend(' ') })
67
elsif stderr.include? 'java.lang.IllegalArgumentException'
68
# STDERR.puts " WARNING: '#{payload_name}' requires complex args and may not be supported"
69
return nil
70
elsif stderr.include? 'Error while generating or serializing payload'
71
# STDERR.puts " WARNING: '#{payload_name}' errored and may not be supported"
72
return nil
73
elsif stdout == "\xac\xed\x00\x05\x70"
74
# STDERR.puts " WARNING: '#{payload_name}' returned null and may not be supported"
75
return nil
76
else
77
# STDERR.puts " Successfully generated #{payload_name} using #{YSOSERIAL_BINARY}"
78
79
# Strip out the semi-randomized ysoserial string and trailing newline
80
payload.gsub!(/#{YSOSERIAL_RANDOMIZED_HEADER}[[:digit:]]{13,14}/, 'ysoserial/Pwner00000000000000')
81
return payload
82
end
83
end
84
85
def generate_payload_array(payload_name)
86
# Generate and return a number of payloads, each with increasingly longer strings, for future comparison
87
payload_array = []
88
(PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i|
89
payload = generate_payload(payload_name, i)
90
return nil if payload.nil?
91
92
payload_array[i] = payload
93
end
94
95
payload_array
96
end
97
98
def length_offset?(current_byte, next_byte)
99
# If this byte has been changed, and is different by one, then it must be a length value
100
if next_byte && current_byte.position == next_byte.position && current_byte.action == '-' && (next_byte.element.ord - current_byte.element.ord == 1)
101
return true
102
end
103
104
false
105
end
106
107
def buffer_offset?(current_byte, next_byte)
108
# If this byte has been inserted, then it must be part of the increasingly large payload buffer
109
if (current_byte.action == '+' && (next_byte.nil? || (current_byte.position != next_byte.position)))
110
return true
111
end
112
113
false
114
end
115
116
def diff(blob_a, blob_b)
117
return nil if blob_a.nil? || blob_b.nil?
118
119
diffs = Diff::LCS.diff(blob_a, blob_b)
120
diffs.flatten(1)
121
end
122
123
def get_payload_list
124
# Call ysoserial and return the list of payloads that can be generated
125
payloads = `java -jar ysoserial-original.jar 2>&1`
126
payloads.encode!('ASCII', 'binary', invalid: :replace, undef: :replace, replace: '')
127
payloads = payloads.split("\n")
128
129
# Make sure the headers are intact, then skip over them
130
abort unless payloads[0] == 'Y SO SERIAL?'
131
payloads = payloads.drop_while { |line| !line.strip.start_with?('Payload') }
132
payloads = payloads.drop(2)
133
134
payload_list = []
135
payloads.each do |line|
136
# Skip the header rows
137
next unless line.start_with? ' '
138
139
payload_list.push(line.match(/^ +([^ ]+)/)[1])
140
end
141
142
payload_list - ['JRMPClient', 'JRMPListener']
143
end
144
145
# YSOSERIAL_MODIFIED_TYPES.unshift(YSOSERIAL_ORIGINAL_TYPE)
146
def generated_ysoserial_payloads
147
results = {}
148
@payload_list.each do |payload|
149
warn "Generating payloads for #{payload}..."
150
151
empty_payload = generate_payload(payload, 0)
152
153
if empty_payload.nil?
154
warn " ERROR: Errored while generating '#{payload}' and it will not be supported"
155
results[payload] = { status: 'unsupported' }
156
next
157
end
158
159
payload_array = generate_payload_array(payload)
160
161
length_offsets = []
162
buffer_offsets = []
163
164
# Comparing diffs of various payload lengths to find length and buffer offsets
165
(PAYLOAD_TEST_MIN_LENGTH..PAYLOAD_TEST_MAX_LENGTH).each do |i|
166
# Compare this binary with the next one
167
diffs = diff(payload_array[i], payload_array[i + 1])
168
169
break if diffs.nil?
170
171
# Iterate through each diff, searching for offsets of the length and the payload
172
diffs.length.times do |j|
173
current_byte = diffs[j]
174
next_byte = diffs[j + 1]
175
prev_byte = diffs[j - 1]
176
177
if j > 0 && (prev_byte.position == current_byte.position)
178
# Skip this if we compared these two bytes on the previous iteration
179
next
180
end
181
182
# Compare this byte and the following byte to identify length and buffer offsets
183
length_offsets.push(current_byte.position) if length_offset?(current_byte, next_byte)
184
buffer_offsets.push(current_byte.position) if buffer_offset?(current_byte, next_byte)
185
end
186
end
187
188
if @debug
189
for length_offset in length_offsets
190
warn " LENGTH OFFSET #{length_offset} = 0x#{empty_payload[length_offset - 1].ord.to_s(16)} #{empty_payload[length_offset].ord.to_s(16)}"
191
end
192
193
for buffer_offset in buffer_offsets
194
warn " BUFFER OFFSET #{buffer_offset}"
195
end
196
warn " PAYLOAD LENGTH: #{empty_payload.length}"
197
end
198
199
payload_bytes = Base64.strict_encode64(empty_payload)
200
if buffer_offsets.empty?
201
# TODO: Turns out ysoserial doesn't have any static payloads. Consider removing this.
202
results[payload] = {
203
status: 'static',
204
bytes: payload_bytes
205
}
206
else
207
results[payload] = {
208
status: 'dynamic',
209
lengthOffset: length_offsets.uniq,
210
bufferOffset: buffer_offsets.uniq,
211
bytes: payload_bytes
212
}
213
end
214
end
215
results
216
end
217
218
@payload_list = get_payload_list
219
unless @ysoserial_payloads.empty?
220
unknown_list = @ysoserial_payloads - @payload_list
221
if unknown_list.empty?
222
@payload_list = @ysoserial_payloads
223
else
224
warn "ERROR: Invalid payloads specified: #{unknown_list.join(', ')}"
225
abort
226
end
227
end
228
229
if @generate_all
230
YSOSERIAL_ALL_TYPES.each do |type|
231
warn "Generating payload type for #{type}..."
232
@payload_type = type
233
@json_document[type] ||= {}
234
@json_document[type].merge!(generated_ysoserial_payloads)
235
$stderr.puts
236
end
237
else
238
@json_document[@payload_type] ||= {}
239
@json_document[@payload_type].merge!(generated_ysoserial_payloads)
240
end
241
242
payload_count = {}
243
payload_count['skipped'] = 0
244
payload_count['static'] = 0
245
payload_count['dynamic'] = 0
246
247
@json_document.each_value do |vs|
248
vs.each_value do |v|
249
case v[:status]
250
when 'unsupported'
251
payload_count['skipped'] += 1
252
when 'static'
253
payload_count['static'] += 1
254
when 'dynamic'
255
payload_count['dynamic'] += 1
256
end
257
end
258
end
259
260
unless @debug
261
puts JSON.pretty_generate(@json_document)
262
end
263
264
warn "DONE! Successfully generated #{payload_count['static']} static payloads and #{payload_count['dynamic']} dynamic payloads. Skipped #{payload_count['skipped']} unsupported payloads."
265
266